IBMメインフレームのファイルをJavaで入出力するには？

# IBMメインフレームのファイルをJavaで入出力するには？
IBMメインフレームのファイルをJavaで入出力するには、下記のようにいくつか面倒なことがあります。

* 文字コードは、半角文字は、EBCDIC。
* 全角文字はIBM漢字コード。シフトアウト(0x0E)、シフトイン(0x0F)で括られた範囲のバイトが全角文字。
* 改行文字がないので、レコード区切りはレコードのバイト数を意識して区切る必要あり。
* 符号付きパック10進数、符号無しパック10進数のいわゆる2進化10進数をBigDecimalにするのは、そこそこ面倒。

で、自作するかどうか考えていたところ、非常に便利なユーティリティクラスが、オープンソースとして提供されているのを見つけたので、その使い方のご紹介です。
<font color="red">**※便利なのに、日本語の情報が少ない。もったいない。**</font>

#<font color="red"> **IBM Toolbox for Java**</font>
名前からして、THEメインフレームの感じです。

##インストールとセットアップ
 * 下記のダウンロードページから、OSSバージョンのjarが格納されたZIPファイル(例えば、jtopen_9_3.zip)をダウンロードします。<br>
 * 「jtopen_9_3.zip」ファイルを解凍し、「lib\java8\jt400.jar」のjarファイルを取り出して、クラスパスに通します。<br>

##プログラミングに関する情報源など
 * IBM Toolbox for Java<BR>
IBM提供の概要説明。<BR>
URL:[https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/page1.htm](https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/page1.htm)<BR>
 * プログラミング IBM Toolbox for Java<BR>
プログラミングお作法などの概要説明したPDFファイル。これがプログラミングの手助けになる。今回の投稿もこれを参考にしながら書いています(このPDFファイルはエッセンスだけしか書いていないので、試行錯誤が必要でした)。<BR>
URL:[https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/rzahhpdf.pdf](https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/rzahhpdf.pdf)<BR>
 * IBM Toolbox for Java API Document<BR>
 URL:[https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzahh/javadoc/index.html](https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzahh/javadoc/index.html)<BR>
 * JTOpen: The Open Source version of the IBM Toolbox for Java<BR>
 オープンソース版の概要説明。<BR>
URL:[http://jt400.sourceforge.net/](http://jt400.sourceforge.net/)<BR>
 * JTOpen: The Open Source version of the IBM Toolbox for Javaのダウンロードページ<BR>
 URL:[https://sourceforge.net/projects/jt400/](https://sourceforge.net/projects/jt400/)<BR>
 * その最新版のダウンロードURL<BR>
 URL:[https://sourceforge.net/projects/jt400/files/latest/download?source=files](https://sourceforge.net/projects/jt400/files/latest/download?source=files)<BR>
 * JTOpenとIBM Toolbox for Javaのライセンス<BR>
 どちらも「IBM Public License Version 1.0」。<BR>
URL：[https://opensource.org/licenses/ibmpl.php](https://opensource.org/licenses/ibmpl.php)<BR>

# IBMメインフレームのファイルをJavaで入出力するための要件定義
ツールの紹介の前に、簡単にこのツールに期待している事、つまり要件を定義し、この要件に従って、ツールの使い方を紹介します。

1. IBMメインフレームのファイルの入出力に使うレコードフォーマット定義は、Javaプログラムの外部に定義し、その定義に従って、レコードの区切りや、項目の区切りを行いたい。
2. 文字コード変換も行いたい。
3. 符号付きパック10進数、符号無しパック10進数のいわゆる2進化10進数をBigDecimalとして扱いたい。

##例題：IBMメインフレームファイルをJavaで入出力
次のようなIBMメインフレームファイルのレコードフォーマをJavaで入出力する前提とします。

|レベル|データ項目名称|型　|桁数|精度|繰返|バイト|備考|
|:---|:-----------|:--:|---:|---:|---:|--:|:---|
|01  |ORDER-REC   |    |    |    |    |124|受注レコード|
|03  |REC-NO      |B   |   4|    |    |  2|レコード番号|
|03  |ORDER-NO    |C   |  10|    |    | 10|受注番号|
|03  |ZIP-CD      |C   |   8|    |    |  8|郵便番号|
|03  |ADDRESS     |C   |  20|    |    | 20|住所(IBM漢字コードで18バイトに、シフトコードで合計20バイト)|
|03  |PRODUCT-GR  |S   |    |    |   3| 84|商品グループ項目(OCCCURS 3回)|
|05  |PRODUCT-CD  |C   |  15|    |    | 15|商品コード|
|05  |PRODUCT-AMT |Z   |   5|   2|    |  5|商品重量(ゾーン10進数)|
|05  |UNIT-PRC    |P   |   7|   2|    |  4|単価(パック10進数)|
|05  |DISCOUNT-PRC|P   |   7|   2|    |  4|割引額(パック10進数)|

型の凡例:

|型|説明|
|:---|:---|
|B |バイナリ型。上記の「レコード番号」をCOBOLで書くと、「S9(4) COMP-4」。バイナリの4桁としていますので、バイト数は2バイトになる。|
|C |文字型。上記の「受注番号」をCOBOLで書くと、「X(10)」。|
|S |グループ項目。複数のデータ項目を括っている。|
|P |パック10進数型。上記の「単価」をCOBOLで書くと、「S9(7)V9(2) COMP-3」。|
|Z |ゾーン10進数型。上記の「商品重量」をCOBOLで書くと、「9(5)V9(2)」。|

ゾーン10進数(外部10進数、アンパック10進数とも言う)、パック10進数(内部10進数)などの解説は、次のURL等を参考。
[https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%83%E3%82%AF10%E9%80%B2%E6%95%B0](https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%83%E3%82%AF10%E9%80%B2%E6%95%B0 )
[https://www.ibm.com/support/knowledgecenter/ja/SSQ2R2_9.1.0/com.ibm.etools.cbl.win.doc/topics/cpari20e.htm#header_19](https://www.ibm.com/support/knowledgecenter/ja/SSQ2R2_9.1.0/com.ibm.etools.cbl.win.doc/topics/cpari20e.htm#header_19)

## レコードフォーマット定義ファイル(RFMLファイル)を作り、クラスパス上に置きます。
メインフレームファイルのレコードフォーマット定義ファイルを作り、それに従ってファイルの入出力できるようにします。
IBM ToolBox for Javaでは、レコードフォーマット定義ファイルとして、XML形式のRFMLファイルを元にファイルの入出力ができますので、
まずは、それを作り、クラスパス上に置きます。後で、Javaプログラムから、この定義ファイルをクラスパスから取得するためです。

「プログラミング IBM Toolbox for Java 」にこのXMLファイルのDTDが書いてあります。
「ccsid="930"」と書いてあるところに注目してください。
ルートレベルの所に書いてあるのと、「ADRESS」の所に書いてあります。
これは、メインフレームの文字コードを指定しているのですが、全体の文字コード指定をする場合は、このルートレベルの所に書き、
個別に指定する場合は、各データ項目に指定することができます。

```xml:ORDER-FILE.rfml(メインフレームのレコードフォーマット定義ファイル)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE rfml SYSTEM "rfml.dtd">
<rfml version="4.0" ccsid="930">
  <struct name="PRODUCT-GR">
    <data name="PRODUCT-CD" type="char" length="15"/>
    <data name="PRODUCT-AMT" type="zoned" length="5" precision="2"/>
    <data name="UNIT-PRC" type="packed" length="7" precision="2"/>
    <data name="DISCOUNT-PRC" type="packed" length="7" precision="2"/>
  </struct>
  <recordformat name="ORDER01-REC">
    <data name="REC-NO" type="int" length="2"/>
    <data name="ORDER-NO" type="char" length="10"/>
    <data name="ZIP-CD" type="char" length="8"/>
    <data name="ADRESS" type="char" length="20" ccsid="930"/>
    <data name="PRODUCT-GR" type="struct" struct="PRODUCT-GR" count="3"/>
  </recordformat>
</rfml>
```
## メインフレームファイル出力その１(失敗例)
下記は、失敗例。StructureとArrayはサポート外とのこと。つまり、グループ項目とOCCURSは使えないということ。どうするか？

```Java:メインフレームファイル出力その１(失敗例)
    /**
     * メインフレームファイル出力. <BR>
     * RFMLファイルのレコードフォーマット定義に従ってメインフレームファイルを出力します。<BR>
     * {@code
      rec.setField("PRODUCT-GR", null);}の所で、 {@code
    com.ibm.as400.access.ExtendedIllegalArgumentException: name (PRODUCT-GR): Field was not found.
    }<BR>
     * 例外が発生します。 次のAPIドキュメントに、Structureはサポート外とある。Arrayもサポート外とある。<BR>
     * <a href=
     * "https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzahh/javadoc/com/ibm/as400/data/RecordFormatDocument.html">
     * https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzahh/javadoc/com/ibm/as400/data/RecordFormatDocument.html</a>
     */
    @Test
    public void test01MfFileOutput() {
        // レコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument("jp.golden.cat.util.ORDER-FILE.rfml");
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }

        // メインフレームの文字コードでファイル出力。
        File ioFile = new File("./data/ORDER_FILE.dat");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            Record rec = rfd.toRecordFormat("ORDER01-REC").getNewRecord();
            for (short i = 1; i < 11; i++) {
                rec.setField("REC-NO", new Short(i));
                rec.setField("ORDER-NO", "170423-123");
                rec.setField("ZIP-CD", "103-0028");
                rec.setField("ADRESS", "東京都中央区八重洲");
                // ↓ここで例外が発生するので、コメントアウト。
                // rec.setField("PRODUCT-GR", null);
                // バイト配列化して、ファイルに出力。
                byte[] buf = rec.getContents();
                StringUtil.dump("DEBUG:", buf, "");
                fos.write(buf);
            }
            fos.flush();
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
```

## メインフレームファイル出力その２(成功例)
グループ項目とOCCURSが使えないので、次のように処理します。

1.グループ項目とOCCURSが意図した挙動にならないので、その部分を一つのバイト配列と見做します。<BR>
2.グループ項目部分を別のレコードフォーマット定義で定義して、そこにJavaオブジェクトを設定して、MF文字コードのバイト配列にします。<br>
3．上記2を上記1.にセットします。

```xml:ORDER-FILE.rfml(メインフレームのレコードフォーマット定義ファイル。グループ項目部分を別のレコード定義にしている。)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE rfml SYSTEM "rfml.dtd">
<rfml version="4.0" ccsid="930">
  <recordformat name="PRODUCT-GR">
    <data name="PRODUCT-CD" type="char" length="15"/>
    <data name="PRODUCT-AMT" type="zoned" length="5" precision="2"/>
    <data name="UNIT-PRC" type="packed" length="7" precision="2"/>
    <data name="DISCOUNT-PRC" type="packed" length="7" precision="2"/>
  </recordformat>
  <recordformat name="ORDER01-REC">
    <data name="REC-NO" type="int" length="2"/>
    <data name="ORDER-NO" type="char" length="10"/>
    <data name="ZIP-CD" type="char" length="8"/>
    <data name="ADRESS" type="char" length="20" ccsid="930"/>
    <data name="PRODUCT-B" type="byte" length="84"/>
  </recordformat>
</rfml>
```

```Java:メインフレームファイル出力その２(成功例)
    /**
     * メインフレームファイル出力. <BR>
     * RFMLファイルのレコードフォーマット定義に従ってメインフレームファイルを出力します。<BR>
     * 1.グループ項目とOCCURSが意図した挙動にならないので、その部分を一つのバイト配列と見做します。<BR>
     * 2.グループ項目部分を別のレコードフォーマット定義で定義して、そこにJavaオブジェクトを設定して、MF文字コードのバイト配列にします。<br>
     * 3．上記2を上記1.にセットします。
     */
    @Test
    public void test02MfFileOutput() {
        // レコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument("jp.golden.cat.util.ORDER-FILE.rfml");
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }

        // メインフレームの文字コードでファイル出力。
        File ioFile = new File("./data/ORDER_FILE.dat");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            Record rec = rfd.toRecordFormat("ORDER01-REC").getNewRecord();
            Record grp = rfd.toRecordFormat("PRODUCT-GR").getNewRecord();
            for (int i = 1; i < 11; i++) {
                rec.setField("REC-NO", new Short((short) i));
                rec.setField("ORDER-NO", "170423-123");
                rec.setField("ZIP-CD", "103-0028");
                rec.setField("ADRESS", "東京都中央区八重洲");

                // グループ項目のOCCURS部分のコード。
                // 項目移送先のグループ項目全体のバイトストリーム。
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                // 項目移送先のバイト長÷1 OCCURS分のバイト長(PRODUCT-GR)＝OCCCURS数
                int occursCount = rec.getFieldAsBytes("PRODUCT-B").length / grp.getRecordLength();
                // OCCURS数分繰り返す
                for (int j = 0; j < occursCount; j++) {
                    grp.setField("PRODUCT-CD", "ABCDEFGHIJ-000" + j);
                    grp.setField("PRODUCT-AMT", new BigDecimal("123.45"));
                    grp.setField("UNIT-PRC", new BigDecimal("+12345.67"));
                    grp.setField("DISCOUNT-PRC", new BigDecimal("-45.6"));

                    baos.write(grp.getContents());
                }
                // グループ項目のバイト配列にセット。
                rec.setField("PRODUCT-B", baos.toByteArray());

                // バイト配列化して、ファイルに出力。
                byte[] buf = rec.getContents();
                StringUtil.dump("DEBUG:", buf, "");
                fos.write(buf);
            }
            fos.flush();
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
```

## メインフレームファイル入力
グループ項目とOCCURSが使えないので、上記のファイル出力でやったようなイメージで対応します。

```Java:メインフレームファイル入力
    /**
     * メインフレームファイル入力. <BR>
     * RFMLファイルのレコードフォーマット定義に従ってメインフレームファイルを入力します。<BR>
     * 1.グループ項目とOCCURSが意図した挙動にならないので、その部分を一つのバイト配列と見做します。<BR>
     * 2.グループ項目部分を別のレコードフォーマット定義で定義して、そこにMF文字コードのバイト配列をセットして、Javaオブジェクトにします。<br>
     */
    @Test
    public void test03MfFileInput() {
        // メインフレームファイルを作る。
        this.test02MfFileOutput();

        // レコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument("jp.golden.cat.util.ORDER-FILE.rfml");
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }

        // メインフレームの文字コードでファイル入力。
        File ioFile = new File("./data/ORDER_FILE.dat");
        FileInputStream fis = null;
        try {
            Record rec = rfd.toRecordFormat("ORDER01-REC").getNewRecord();
            Record grp = rfd.toRecordFormat("PRODUCT-GR").getNewRecord();
            fis = new FileInputStream(ioFile);
            // 1レコード分のバイト数を取得し、読み込みバッファのバイト配列を生成する。
            byte[] recordBuf = new byte[rec.getRecordLength()];
            // MFファイルには、改行文字がない。従って、1レコードのバイト数分バッファに読み込んでいく。
            while (fis.read(recordBuf) != -1) {
                // バッファのバイト配列からレコード化。
                rec.setContents(recordBuf);

                System.out.print("[" + rec.getField("REC-NO"));
                System.out.print("],[" + rec.getField("ORDER-NO"));

                // グループ項目のOCCURS部分のコード。
                // 項目取得元のグループ項目全体のバイトストリーム。
                ByteArrayInputStream bais = new ByteArrayInputStream((byte[]) rec.getField("PRODUCT-B"));
                // 1 OCCURS分の読み込みバッファを作る。
                byte[] groupBuf = new byte[grp.getRecordLength()];
                while (bais.read(groupBuf) != -1) {
                    grp.setContents(groupBuf);
                    System.out.print("],[" + grp.getField("PRODUCT-CD"));
                    System.out.print("],[" + grp.getField("PRODUCT-AMT"));
                    System.out.print("],[" + grp.getField("UNIT-PRC"));
                    System.out.print("],[" + grp.getField("DISCOUNT-PRC"));
                }
                System.out.println("]");
            }
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
```

### 2017/04/19(水):新たに分かったこと:
rfd.toRecord("ORDER-REC").getRecordLength()は、つぎの例外が発生します。
「com.ibm.as400.data.PcmlException: Value is not set. Processing <data> element 'ORDER-REC.REC-NO'.」
おそらく、RecordFormatDocumentオブジェクトの中にRecordオブジェクトが生成されていなため、Lengthを取得することができないと思われます。getNewRecord()でオブジェクトを生成してからLengthを取得するようにサンプルコードを修正しています。
### 2017/04/19(水):バグ修正:
バッファに読み込んだ後に、RecordFormatDocumentオブジェクトにバッファをセットしていなかった。
while (fis.read(buffer) != -1) {
  <font color="red">rfd.setValues("ORDER-REC", buffer);</font>
### 2017/04/19(水):新たに分かったこと(getValue()メソッドは勝手にtrimする):
次のコードは、どちらもORDER-NOTEを取り出すメソッドであるが、結果が異なる。
(1) rfd.getValue("ORDER-REC.ORDER-NOTE")
(2) rec.getField("ORDER-NOTE")
(1)は、Stringの末尾スペースが勝手に削除される。(2)は、末尾スペースがあれば、もとのまま残る。
(1)は、内部でPcmlDataValues.toObject()を呼び出している模様。その中で、trimStringしている。

```java:PcmlDataValues.toObject()
//For Strings, trim blanks and nulls appropriately
if (dataType == PcmlData.CHAR) {
    newVal = trimString((String)newVal, getTrim());
}
```
getValue()は、勝手なことをしているので、getField()メソッドを使った方が良い。
### 2017/04/23(日):新たに分かったこと:
AS400Arrayは、RFMLファイル書き込みことはできるが、RFMLファイルに読み込むと、そのOCCURS数が再現されない。
AS400Structreは、RFMLファイル書き込みできない。RFMLファイルで読み込んで使おうとすると例外が起きる。

### 2017/04/26(水):グループ項目の入出力見直し:
ファイルの入出力にStreamを使っているなら、グループ項目のバイト配列の入出力もStreamで行う方が全体としての統一感がでる。


## その他確認につかったコード全体

``` java:IBMToolboxTest.java
package jp.golden.cat.util;

import static org.junit.Assert.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharConversionException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.ibm.as400.access.AS400Array;
import com.ibm.as400.access.AS400Bin4;
import com.ibm.as400.access.AS400ByteArray;
import com.ibm.as400.access.AS400DataType;
import com.ibm.as400.access.AS400PackedDecimal;
import com.ibm.as400.access.AS400Structure;
import com.ibm.as400.access.AS400Text;
import com.ibm.as400.access.ArrayFieldDescription;
import com.ibm.as400.access.BinaryFieldDescription;
import com.ibm.as400.access.CharacterFieldDescription;
import com.ibm.as400.access.HexFieldDescription;
import com.ibm.as400.access.PackedDecimalFieldDescription;
import com.ibm.as400.access.Record;
import com.ibm.as400.access.RecordFormat;
import com.ibm.as400.data.RecordFormatDocument;
import com.ibm.as400.data.XmlException;

/**
 * IBM Toolbox for Javaのテスト.<BR>
 *
 * <H1>インストールとセットアップ</H1><BR>
 * 下記のダウンロードページから、OSSバージョンのjarが格納されたZIPファイル(例えば、jtopen_9_3.zip)をダウンロードします。<br>
 * 「jtopen_9_3.zip\lib\java8\jt400.jar」をクラスパスに通します。<br>
 * <br>
 * <H1>情報源など</H1> <BR>
 * IBM Toolbox for Java。IBM提供の概要説明。 URL:<a href=
 * "https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/page1.htm">https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/page1.htm</a><BR>
 * プログラミング IBM Toolbox for Java。PDFファイル。プログラミングお作法などの概要説明。 URL:<a href=
 * "https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/rzahhpdf.pdf">https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/rzahh/rzahhpdf.pdf</a><BR>
 * IBM Toolbox for Java API Document。URL:<a href=
 * "https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzahh/javadoc/index.html">https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzahh/javadoc/index.html</a><BR>
 * JTOpen: The Open Source version of the IBM Toolbox for
 * Java。オープンソース版の概要説明。URL:<a href=
 * "http://jt400.sourceforge.net/">http://jt400.sourceforge.net/</a><BR>
 * JTOpen: The Open Source version of the IBM Toolbox for
 * Javaのダウンロードページ。URL:<a href=
 * "https://sourceforge.net/projects/jt400/">https://sourceforge.net/projects/jt400/</a><BR>
 * 最新版のダウンロードURL。URL:<a href=
 * "https://sourceforge.net/projects/jt400/files/latest/download?source=files">https://sourceforge.net/projects/jt400/files/latest/download?source=files</a><BR>
 * JTOpenとIBM Toolbox for Javaのライセンス。どちらも「IBM Public License Version
 * 1.0」。URL：<a href=
 * "https://opensource.org/licenses/ibmpl.php">https://opensource.org/licenses/ibmpl.php</a><BR>
 */
public class IBMToolboxTest {
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    /**
     * メインフレームファイル出力. <BR>
     * RFMLファイルのレコードフォーマット定義に従ってメインフレームファイルを出力します。<BR>
     * {@code
      rec.setField("PRODUCT-GR", null);}の所で、 {@code
    com.ibm.as400.access.ExtendedIllegalArgumentException: name (PRODUCT-GR): Field was not found.
    }<BR>
     * 例外が発生します。 次のAPIドキュメントに、Structureはサポート外とある。Arrayもサポート外とある。<BR>
     * <a href=
     * "https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzahh/javadoc/com/ibm/as400/data/RecordFormatDocument.html">
     * https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzahh/javadoc/com/ibm/as400/data/RecordFormatDocument.html</a>
     */
    @Test
    public void test01MfFileOutput() {
        // レコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument("jp.golden.cat.util.ORDER-FILE.rfml");
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }

        // メインフレームの文字コードでファイル出力。
        File ioFile = new File("./data/ORDER_FILE.dat");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            Record rec = rfd.toRecordFormat("ORDER01-REC").getNewRecord();
            for (short i = 1; i < 11; i++) {
                rec.setField("REC-NO", new Short(i));
                rec.setField("ORDER-NO", "170423-123");
                rec.setField("ZIP-CD", "103-0028");
                rec.setField("ADRESS", "東京都中央区八重洲");
                // ↓ここで例外が発生するので、コメントアウト。
                // rec.setField("PRODUCT-GR", null);
                // バイト配列化して、ファイルに出力。
                byte[] buf = rec.getContents();
                StringUtil.dump("DEBUG:", buf, "");
                fos.write(buf);
            }
            fos.flush();
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * メインフレームファイル出力. <BR>
     * RFMLファイルのレコードフォーマット定義に従ってメインフレームファイルを出力します。<BR>
     * 1.グループ項目とOCCURSが意図した挙動にならないので、その部分を一つのバイト配列と見做します。<BR>
     * 2.グループ項目部分を別のレコードフォーマット定義で定義して、そこにJavaオブジェクトを設定して、MF文字コードのバイト配列にします。<br>
     * 3．上記2を上記1.にセットします。
     */
    @Test
    public void test02MfFileOutput() {
        // レコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument("jp.golden.cat.util.ORDER-FILE.rfml");
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }

        // メインフレームの文字コードでファイル出力。
        File ioFile = new File("./data/ORDER_FILE.dat");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            Record rec = rfd.toRecordFormat("ORDER01-REC").getNewRecord();
            Record grp = rfd.toRecordFormat("PRODUCT-GR").getNewRecord();
            for (int i = 1; i < 11; i++) {
                rec.setField("REC-NO", new Short((short) i));
                rec.setField("ORDER-NO", "170423-123");
                rec.setField("ZIP-CD", "103-0028");
                rec.setField("ADRESS", "東京都中央区八重洲");

                // グループ項目のOCCURS部分のコード。
                // 項目移送先のグループ項目全体のバイトストリーム。
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                // 項目移送先のバイト長÷1 OCCURS分のバイト長(PRODUCT-GR)＝OCCCURS数
                int occursCount = rec.getFieldAsBytes("PRODUCT-B").length / grp.getRecordLength();
                // OCCURS数分繰り返す
                for (int j = 0; j < occursCount; j++) {
                    grp.setField("PRODUCT-CD", "ABCDEFGHIJ-000" + j);
                    grp.setField("PRODUCT-AMT", new BigDecimal("123.45"));
                    grp.setField("UNIT-PRC", new BigDecimal("+12345.67"));
                    grp.setField("DISCOUNT-PRC", new BigDecimal("-45.6"));

                    baos.write(grp.getContents());
                }
                // グループ項目のバイト配列にセット。
                rec.setField("PRODUCT-B", baos.toByteArray());

                // バイト配列化して、ファイルに出力。
                byte[] buf = rec.getContents();
                StringUtil.dump("DEBUG:", buf, "");
                fos.write(buf);
            }
            fos.flush();
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * メインフレームファイル入力. <BR>
     * RFMLファイルのレコードフォーマット定義に従ってメインフレームファイルを入力します。<BR>
     * 1.グループ項目とOCCURSが意図した挙動にならないので、その部分を一つのバイト配列と見做します。<BR>
     * 2.グループ項目部分を別のレコードフォーマット定義で定義して、そこにMF文字コードのバイト配列をセットして、Javaオブジェクトにします。<br>
     */
    @Test
    public void test03MfFileInput() {
        // メインフレームファイルを作る。
        this.test02MfFileOutput();

        // レコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument("jp.golden.cat.util.ORDER-FILE.rfml");
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }

        // メインフレームの文字コードでファイル入力。
        File ioFile = new File("./data/ORDER_FILE.dat");
        FileInputStream fis = null;
        try {
            Record rec = rfd.toRecordFormat("ORDER01-REC").getNewRecord();
            Record grp = rfd.toRecordFormat("PRODUCT-GR").getNewRecord();
            fis = new FileInputStream(ioFile);
            // 1レコード分のバイト数を取得し、読み込みバッファのバイト配列を生成する。
            byte[] recordBuf = new byte[rec.getRecordLength()];
            // MFファイルには、改行文字がない。従って、1レコードのバイト数分バッファに読み込んでいく。
            while (fis.read(recordBuf) != -1) {
                // バッファのバイト配列からレコード化。
                rec.setContents(recordBuf);

                System.out.print("[" + rec.getField("REC-NO"));
                System.out.print("],[" + rec.getField("ORDER-NO"));

                // グループ項目のOCCURS部分のコード。
                // 項目取得元のグループ項目全体のバイトストリーム。
                ByteArrayInputStream bais = new ByteArrayInputStream((byte[]) rec.getField("PRODUCT-B"));
                // 1 OCCURS分の読み込みバッファを作る。
                byte[] groupBuf = new byte[grp.getRecordLength()];
                while (bais.read(groupBuf) != -1) {
                    grp.setContents(groupBuf);
                    System.out.print("],[" + grp.getField("PRODUCT-CD"));
                    System.out.print("],[" + grp.getField("PRODUCT-AMT"));
                    System.out.print("],[" + grp.getField("UNIT-PRC"));
                    System.out.print("],[" + grp.getField("DISCOUNT-PRC"));
                }
                System.out.println("]");
            }
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * COMP-4からJavaオブジェクト(Integer)に変換するテスト.
     */
    @Test
    public void test01() {
        // 入力値(MFバイトデータ)
        // IBMメインフレームもJavaもエンディアンはビッグエンディアンなので、バイトオーダーは気にしなくてよい。
        byte[] mfData = new byte[] { 0x0A, 0x0B, 0x0C, 0x0D };
        StringUtil.dump("test01:input   :", mfData, "");

        // COMP-4からJavaオブジェクト(Ｉｎｔｅｇｅｒ)に変換する。
        AS400Bin4 comp4 = new AS400Bin4();
        int actual = ((Integer) comp4.toObject(mfData, 0)).intValue();
        System.out.println("test01:actual  :" + actual);

        // 期待値作成。
        int expected = 168496141;
        System.out.println("test01:expected:" + expected);

        // 0x0A0B0C0Dは、10進数の168496141。
        assertEquals(expected, actual);
    }

    /**
     * Javaオブジェクト(Integer)からCOMP-4に変換するテスト.
     */
    @Test
    public void test02() {
        // 入力値(Javaオブジェクト)
        Integer javaObject = new Integer(168496141);
        System.out.println("test02:input   :" + javaObject);

        // Javaオブジェクト(Ｉｎｔｅｇｅｒ)からCOMP-4に変換する。
        AS400Bin4 comp4 = new AS400Bin4();
        byte[] actual = comp4.toBytes(javaObject);
        StringUtil.dump("test02:actual  :", actual, "");

        // 期待値作成。
        byte[] expected = new byte[] { 0x0A, 0x0B, 0x0C, 0x0D };
        StringUtil.dump("test02:expected:", expected, "");

        // test
        assertArrayEquals(expected, actual);
    }

    /**
     * PIC X形式のEBCIDICからMS932(SJIS)に変換するテスト. <BR>
     *
     * IBM Globalization - Coded character set identifiers.UR:<a href=
     * "https://www-01.ibm.com/software/globalization/ccsid/ccsid_registered.html">https://www-01.ibm.com/software/globalization/ccsid/ccsid_registered.html</a><br>
     *
     * IBM Globalization - Coded character set identifiers - CCSID
     * 5035。URL:<a href=
     * "https://www-01.ibm.com/software/globalization/ccsid/ccsid5035.html">https://www-01.ibm.com/software/globalization/ccsid/ccsid5035.html</a><br>
     *
     * CCSID 5035 の代替 Unicode 変換表.URL：<a href=
     * "https://www.ibm.com/support/knowledgecenter/ja/SSEPGG_9.5.0/com.ibm.db2.luw.admin.nls.doc/doc/c0022611.html">https://www.ibm.com/support/knowledgecenter/ja/SSEPGG_9.5.0/com.ibm.db2.luw.admin.nls.doc/doc/c0022611.html</a><br>
     *
     * 79.英小文字を入力するには CCSID 5035.URL:<a href=
     * "http://www.as400-net.com/tips/environment/79.html">http://www.as400-net.com/tips/environment/79.html</a><br>
     *
     * CCSID=930 「4370 UDC
     * 文字を含む日本語カタカナ漢字、5026のスーパーセット。半角カナ優先(半角英小文字を混在できない)。<BR>
     * CCSID=939 「4370 UDC
     * 文字を含む日本語ラテン文字漢字、5035のスーパーセット。半角英小文字優先(半角カナ文字を混在できない)。<BR>
     */
    @Test
    public void test03() {
        // MFの文字コード、CCSID=930 「4370 UDC
        // 文字を含む日本語カタカナ漢字、5026のスーパーセット。半角カナ優先(半角英小文字を混在できない)のバイト配列。
        // "123AαBｱ"=0xF1F2F3C10E41410FC281。
        // 半角カタカナの"ｱ"は、CCSID=930は、0x81。これが、CCSID=939だ0x56。
        // 全角文字の"α"は、MFの文字コードで0X4141。これをシフトアウト(0x0E)とシフトイン(0x0F)でくくり、0x0E41410F。
        byte[] mfData = new byte[] { (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xC1, 0x0E, 0x41, 0x41, 0x0F,
                (byte) 0xC2, (byte) 0x81 };
        StringUtil.dump("test03:input   :", mfData, "");
        System.out.println("test03:input   バイト数:" + mfData.length);

        // CCSID=930 「4370 UDC
        // 文字を含む日本語カタカナ漢字、5026のスーパーセット。半角カナ優先(半角英小文字を混在できない)のバイト配列を
        // Unicodeに変換します。
        int ccsid = 930;
        AS400Text xtype = new AS400Text(mfData.length, ccsid);
        String unicodeString = (String) xtype.toObject(mfData);
        System.out.println("test03:unicodeStringバイト数:" + unicodeString.length());
        StringUtil.dump("test03:unicodeString:", unicodeString.getBytes(), "");

        // UnicodeをMS932(SJIS)に変換します。
        String encoding = "MS932";
        String actual = this.convertString(unicodeString, encoding);
        System.out.println("test03:actual  :" + actual);
        // シフトアウト(0x0E)とシフトイン(0x0F)がないので、2バイト少なくなる。
        System.out.println("test03:actual  バイト数:" + this.getStringByteLength(actual, encoding));
        StringUtil.dump("test03:actual  :", this.getStringBytes(actual, encoding), "");

        // 期待値を作ります。
        String expected = this.convertString("123AαBｱ", encoding);
        System.out.println("test03:expected:" + expected);
        // シフトアウト(0x0E)とシフトイン(0x0F)がないので、2バイト少なくなる。
        System.out.println("test03:expectedバイト数:" + this.getStringByteLength(expected, encoding));
        StringUtil.dump("test03:expected:", this.getStringBytes(expected, encoding), "");

        // test
        assertEquals(expected, actual);
    }

    /**
     * 文字コード変換.
     *
     * @param from 変換元の文字列。
     * @param encoding 変換後の文字コード。
     * @return 変換後の文字列。
     */
    String convertString(String from, String encoding) {
        String res = null;
        try {
            res = new String(getStringBytes(from, encoding), encoding);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        return res;
    }

    /**
     * 文字コード変換し、バイト配列で返します.
     *
     * @param from 変換元の文字列。
     * @param encoding 変換後の文字コード。
     * @return 変換後のバイト配列。
     */
    byte[] getStringBytes(String from, String encoding) {
        byte[] res = null;
        try {
            res = from.getBytes(encoding);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        return res;
    }

    /**
     * 文字コード変換後のバイト数を返します.
     *
     * @param from 変換元の文字列。
     * @param encoding 変換後の文字コード。
     * @return 文字コード変換後のバイト数。
     */
    int getStringByteLength(String from, String encoding) {
        return getStringBytes(from, encoding).length;
    }

    /**
     * 複合データタイプの変換テスト.
     */
    @Test
    public void test04() {
        // Java複合オブジェクト
        Object[] javaObjects = {
                new Integer(88),
                new byte[] { 0x41 },
                new BigDecimal("-12.34"),
                "ABCDE"
        };

        // MF複合データ定義
        AS400DataType[] mfDataType = {
                new AS400Bin4(),
                new AS400ByteArray(4),
                new AS400PackedDecimal(4, 2),
                new AS400Text(10)
        };

        // 引数で指定されたMF複合データ定義でコンバータを生成します。
        AS400Structure converter = new AS400Structure(mfDataType);

        // Java複合オブジェクトからMF文字コード変換したバイト配列を生成します。
        byte[] outAS400 = converter.toBytes(javaObjects);
        StringUtil.dump("test04:", outAS400, "");

        Object[] outJavaObjects = (Object[]) converter.toObject(outAS400, 0);
        System.out.println("[" + outJavaObjects[0] + "]");
        StringUtil.dump("[", (byte[]) outJavaObjects[1], "]");    // 受け側4byteに対して、0x41をセットすると、左詰めでセットされる。残りは0ｘ00で埋めらる。
        System.out.println("[" + outJavaObjects[2] + "]");
        System.out.println("[" + outJavaObjects[3] + "]");  // 受け側10byteに対して、ABCDEの5文字をセットすると、左爪でセットされる。残りは、スペース。

        // test
        assertEquals(88, outJavaObjects[0]);
        assertArrayEquals(new byte[] { 0x41, 0x00, 0x00, 0x00 }, (byte[]) outJavaObjects[1]);
        assertEquals(new BigDecimal("-12.34"), outJavaObjects[2]);
        assertEquals("ABCDE     ", outJavaObjects[3]);
    }

    /**
     * MFレコードフォーマット定義オブジェクトを生成し、それに従って値をセットするテスト.
     */
    @Test
    public void test05() {
        // 項目定義
        BinaryFieldDescription bfd = new BinaryFieldDescription(new AS400Bin4(), "REC-NO");
        CharacterFieldDescription cfd1 = new CharacterFieldDescription(new AS400Text(10), "ORDER-NO");
        CharacterFieldDescription cfd2 = new CharacterFieldDescription(new AS400Text(20), "ORDER-NOTE");

        // レコードフォーマット定義
        RecordFormat rf = new RecordFormat();
        rf.addFieldDescription(bfd);
        rf.addFieldDescription(cfd1);
        rf.addFieldDescription(cfd2);

        // レコード生成し、Javaオブジェクトの値をセット。
        Record rec = rf.getNewRecord();
        rec.setField("REC-NO", new Integer(1));
        rec.setField("ORDER-NO", "170415-123");
        rec.setField("ORDER-NOTE", "HELLO WORLD");

        // MFバイト配列を取得。
        byte[] actual = null;
        try {
            actual = rec.getContents();
        } catch (CharConversionException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        // 期待値。
        byte[] expected = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0xF1, (byte) 0xF7,
                (byte) 0xF0, (byte) 0xF4, (byte) 0xF1, (byte) 0xF5, (byte) 0x60, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3,
                (byte) 0xC8, (byte) 0xC5, (byte) 0xD3, (byte) 0xD3, (byte) 0xD6, (byte) 0x40, (byte) 0xE6, (byte) 0xD6,
                (byte) 0xD9, (byte) 0xD3, (byte) 0xC4, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,
                (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40 };
        StringUtil.dump("test05:actual  :", actual, "");
        StringUtil.dump("test05:expected:", expected, "");

        // test
        assertArrayEquals(expected, actual);
    }

    /**
     * MFレコードフォーマット定義オブジェクトを生成し、それに従ってEBCIDICバイナリファイルの入出力テスト.
     */
    @Test
    public void test06() {
        RecordFormat rf = this.createOrderRecordFormat();
        Record rec = rf.getNewRecord();

        File ioFile = new File("./data/AS400.dat");

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            // 10レコード生成
            for (int i = 1; i < 11; i++) {
                rec.setField("REC-NO", new Integer(i));
                rec.setField("AMOUNT", new BigDecimal("-12.34"));
                rec.setField("ORDER-NO", "170415-123");
                rec.setField("ORDER-NOTE", "HELLOαWORLD");
                // getContentsの引数にOutputStreamを指定できる。
                // イディオム的にはos.write(rec.getContents())をよく使うので、これで行く。
                fos.write(rec.getContents());
            }
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (CharConversionException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        FileInputStream fis = null;
        try {
            fis = new FileInputStream(ioFile);
            rec = rf.getNewRecord();
            // 1レコード分のバイト数を取得し、バイト配列を生成する。
            int len = rec.getRecordLength();
            byte[] buffer = new byte[len];

            // MFファイルには、改行文字がない。従って、1レコードのバイト数分バッファに読み込んでいく
            while (fis.read(buffer) != -1) {
                rec.setContents(buffer);
                StringUtil.dump("test06:", buffer, "");
                System.out.print(rec.getField("REC-NO"));
                System.out.print("," + rec.getField("ORDER-NO"));
                System.out.print("," + rec.getField("AMOUNT"));
                System.out.println("," + rec.getField("ORDER-NOTE"));
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * MFレコードフォーマット定義ファイルからMFレコードフォーマット定義オブジェクトを生成し、それに従ってEBCDICバイナリファイル出力テスト.
     *
     * @throws XmlException レコードフォーマット例外
     * @throws IOException ファイルIO例外
     */
    @Test
    public void test07() throws XmlException, IOException {
        // MFレコードフォーマット定義を生成する。
        RecordFormat rf = this.createOrderRecordFormat();

        // MFレコードフォーマット定義をクラスパス上に保存する。
        this.saveRecordFormat(rf, new File("src/test/java/jp/golden/cat/util/ORDER.rfml"));

        // MFレコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = this.loadRecordFormatDocument("jp.golden.cat.util.ORDER.rfml");

        // EBCDICで書く。
        File ioFile = new File("./data/AS400_2.dat");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            // 10レコード生成
            for (int i = 1; i < 11; i++) {
                // 項目の指定の仕方に癖がある。「レコードフォーマット定義名.項目名」で項目を指定する。
                rfd.setValue("ORDER-REC.REC-NO", new Integer(i));
                rfd.setValue("ORDER-REC.AMOUNT", new BigDecimal("-12.34"));
                rfd.setValue("ORDER-REC.ORDER-NO", "170415-123");
                rfd.setValue("ORDER-REC.ORDER-NOTE", "HELLOαWORLD");
                fos.write(rfd.toByteArray("ORDER-REC"));
            }
            fos.flush();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * MFレコードフォーマット定義ファイルからMFレコードフォーマット定義オブジェクトを生成し、それに従ってEBCDICバイナリファイル入力テスト.
     *
     * @throws XmlException レコードフォーマット例外
     * @throws IOException ファイルIO例外
     */
    @Test
    public void test08() throws XmlException, IOException {
        // MFレコードフォーマット定義をクラスパスから探して読み込む。
        RecordFormatDocument rfd = this.loadRecordFormatDocument("jp.golden.cat.util.ORDER.rfml");

        // EBCDICファイルを読む。
        File ioFile = new File("./data/AS400_2.dat");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(ioFile);

            // rfd.toRecord("ORDER-REC").getRecordLength()は、つぎの例外が発生する。
            // com.ibm.as400.data.PcmlException: Value is not set. Processing
            // <data> element 'ORDER-REC.REC-NO'.
            // おそらく、RecordFormatDocumentオブジェクトの中にRecordオブジェクトが生成されていなため、Lengthを取得することができないと思われる。
            // よって、getNewRecord()でオブジェクトを生成してからLengthを取得する。
            // int len = rfd.toRecord("ORDER-REC").getRecordLength();

            // 1レコード分のバイト数を取得し、バイト配列を生成する。
            int len = rfd.toRecordFormat("ORDER-REC").getNewRecord().getRecordLength();
            byte[] buffer = new byte[len];

            // MFファイルには、改行文字がない。従って、1レコードのバイト数分バッファに読み込んでいく
            while (fis.read(buffer) != -1) {
                rfd.setValues("ORDER-REC", buffer);

                StringUtil.dump("test08:", rfd.toByteArray("ORDER-REC"), "");

                System.out.print("[" + rfd.getValue("ORDER-REC.REC-NO"));
                System.out.print("],[" + rfd.getValue("ORDER-REC.ORDER-NO"));
                System.out.print("],[" + rfd.getValue("ORDER-REC.AMOUNT"));
                System.out.print("],[" + rfd.getValue("ORDER-REC.ORDER-NOTE"));
                System.out.println("]");

                Record rec = rfd.toRecord("ORDER-REC");
                System.out.print("[" + rec.getField("REC-NO"));
                System.out.print("],[" + rec.getField("ORDER-NO"));
                System.out.print("],[" + rec.getField("AMOUNT"));

                // 次のコードは、どちらもORDER-NOTEを取り出すメソッドであるが、結果が異なる。
                // (1) rfd.getValue("ORDER-REC.ORDER-NOTE")
                // (2) rec.getField("ORDER-NOTE")
                // (1)は、Stringの末尾スペースが勝手に削除される。(2)は、末尾スペースがあれば、もとのまま残る。
                // (1)は、内部でPcmlDataValues.toObject()を呼び出している模様。その中で、trimStringしている。
                /*
                 * //For Strings, trim blanks and nulls appropriately if
                 * (dataType == PcmlData.CHAR) { newVal = trimString((String)
                 * newVal, getTrim()); }
                 */
                // getValue()は、勝手なことをしているので、getField()メソッドを使った方が良い。
                System.out.print("],[" + rec.getField("ORDER-NOTE"));
                System.out.println("]");
            }
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * 構造体のOCCURSはレコードフォーマット定義にできるか？＝＞できない。
     *
     * @throws XmlException レコードフォーマット例外
     * @throws IOException ファイルIO例外
     */
    @Test
    public void test09() throws XmlException, IOException {
        // AS400データタイプ定義
        AS400DataType[] as400Types = new AS400DataType[] {
                new AS400Text(10),
                new AS400PackedDecimal(4, 2),
        };
        AS400Structure as400Structure = new AS400Structure(as400Types);
        AS400Array as400Array = new AS400Array(as400Structure, 3);

        // Javaオブジェクト＝＞AS400データバイト配列
        Object[] inJavaObject = new Object[as400Array.getNumberOfElements()];
        System.out.println("test09:AS400Array elements:" + as400Array.getNumberOfElements());
        for (int i = 0; i < inJavaObject.length; i++) {
            inJavaObject[i] = new Object[] {
                    new String("170415-123"),
                    new BigDecimal("-12.34"),
            };
        }
        byte[] as400bytes = as400Array.toBytes(inJavaObject);
        StringUtil.dump("test09:", as400bytes, "");

        // AS400データタイプ(Structure+Array)レコードフォーマット定義をXMLで保存できるか？＝＞できない。
        ArrayFieldDescription afd = new ArrayFieldDescription(as400Array, "AMOUNTS");
        RecordFormat rf = new RecordFormat("TEST09-REC");
        rf.addFieldDescription(afd);
        File rfml = new File("src/test/java/jp/golden/cat/util/TEST09.rfml");
        FileOutputStream fos = null;
        try {
            // RecordFormatの中に、AS400Structureは、次の例外が発生し、RFMLファイルとして保存できない。
            // com.ibm.as400.data.XmlException: Data type AS400Structure is not
            // supported by RFML.
            // APIドキュメントにもサポート外と書いてあった。多次元のAS400Arrayもダメ見たい。
            // https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzahh/javadoc/com/ibm/as400/data/RecordFormatDocument.html
            thrown.expect(com.ibm.as400.data.XmlException.class);
            thrown.expectMessage("Data type AS400Structure is not supported by RFML.");
            RecordFormatDocument rfd = new RecordFormatDocument(rf);
            fos = new FileOutputStream(rfml);
            rfd.toXml(fos);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }

    /**
     * AS400Array+AS400StructureはRFMLファイルにできないので、バイト数をカウントするまでにとどめておいて、RFMLファイルに保存する。
     *
     */
    @Test
    public void test10() {
        // AS400データタイプ定義
        AS400DataType[] types = new AS400DataType[] {
                new AS400Text(10),
                new AS400PackedDecimal(4, 2),
        };
        AS400Structure a400Structure = new AS400Structure(types);
        AS400Array as400Array = new AS400Array(a400Structure, 3);
        System.out.println("test10:AS400Structure byte length:" + a400Structure.getByteLength());
        System.out.println("test10:AS400Arrat byte length:" + as400Array.getByteLength());
        // AS400Array+AS400Structureのバイト数分のAS400ByteArrayの定義を作る。
        AS400ByteArray as400ByteArray = new AS400ByteArray(as400Array.getByteLength());

        // レコードフォーマット定義を作る。
        BinaryFieldDescription bfd = new BinaryFieldDescription(new AS400Bin4(), "REC-NO");
        CharacterFieldDescription cfd1 = new CharacterFieldDescription(new AS400Text(10), "ORDER-NO");
        HexFieldDescription hfd = new HexFieldDescription(as400ByteArray, "AMOUNTS");   // AS400Array+AS400Structureのバイト数分のAS400ByteArray
        CharacterFieldDescription cfd2 = new CharacterFieldDescription(new AS400Text(20), "ORDER-NOTE");
        RecordFormat rf = new RecordFormat("ORDER3-REC");
        rf.addFieldDescription(bfd);
        rf.addFieldDescription(cfd1);
        rf.addFieldDescription(hfd);
        rf.addFieldDescription(cfd2);

        // RFMLファイルに保存する。
        File rfml = new File("src/test/java/jp/golden/cat/util/ORDER3.rfml");
        this.saveRecordFormat(rf, rfml);
    }

    /**
     * AS400Array+AS400StructureはRFMLファイルにできないので、その部分をバイト配列で表現したRFMLファイルでレコードを出力.
     *
     * @throws XmlException レコードフォーマット例外
     * @throws IOException ファイルIO例外
     */
    @Test
    public void test11() throws XmlException, IOException {
        // AS400データタイプ定義
        AS400DataType[] types = new AS400DataType[] {
                new AS400Text(10),
                new AS400PackedDecimal(4, 2),
        };
        AS400Structure as400structure = new AS400Structure(types);
        AS400Array as400array = new AS400Array(as400structure, 3);

        // RFMLファイルからレコードオブジェクトを生成。
        RecordFormatDocument rfd = this.loadRecordFormatDocument("jp.golden.cat.util.ORDER3.rfml");
        Record rec = rfd.toRecordFormat("ORDER3-REC").getNewRecord();

        // MFファイル出力
        File ioFile = new File("./data/AS4003.dat");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(ioFile);
            // 10レコード生成
            for (int i = 1; i < 11; i++) {
                rec.setField("REC-NO", new Integer(i));
                rec.setField("ORDER-NO", "170415-123");

                // AS400Array+AS400Structureの部分のバイト配列を作る
                // まずは、セットするJavaオブジェクトを作る
                Object[] inJavaObject = new Object[as400array.getNumberOfElements()];
                for (int j = 0; j < inJavaObject.length; j++) {
                    inJavaObject[j] = new Object[] {
                            new String("170415-AA" + j),
                            new BigDecimal("-12.34"),
                    };
                }
                // 2次元のJavaオブジェクトからバイト配列を作るイメージ
                byte[] as400bytes = as400array.toBytes(inJavaObject);
                System.out.println("test11:" + as400bytes.length);
                // そのバイト配列をセット
                rec.setField("AMOUNTS", as400bytes);

                rec.setField("ORDER-NOTE", "HELLOαWORLD");
                fos.write(rec.getContents());
            }
            fos.flush();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * AS400Array+AS400StructureはRFMLファイルにできないので、AS400Structureを1レコードと見做して、RFMLファイルにして、読んでみる。
     *
     * @throws XmlException レコードフォーマット例外
     * @throws IOException ファイルIO例外
     */
    @Test
    public void test12() throws IOException, XmlException {
        // RFMLファイルからレコードオブジェクトを生成。
        RecordFormatDocument rfd = this.loadRecordFormatDocument("jp.golden.cat.util.ORDER4.rfml");
        Record rec1 = rfd.toRecordFormat("ORDER4-REC").getNewRecord();
        // 1 OCCURS分のレコード定義。本当はAS400Structureを使いたいが、サポート外なので。
        Record rec2 = rfd.toRecordFormat("AMOUNTS").getNewRecord();

        // MFファイル入力
        File ioFile = new File("./data/AS4003.dat");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(ioFile);
            // 1レコード分のバイト数を取得し、バイト配列を生成する。
            int len = rec1.getRecordLength();
            byte[] buffer = new byte[len];
            // MFファイルには、改行文字がない。従って、1レコードのバイト数分バッファに読み込んでいく
            while (fis.read(buffer) != -1) {
                rec1.setContents(buffer);

                System.out.print("[" + rec1.getField("REC-NO"));
                System.out.print("],[" + rec1.getField("ORDER-NO"));

                // OCCURS全体のバイト配列を取得
                byte[] amounts = (byte[]) rec1.getField("AMOUNTS");
                // OCCURS全体のバイト長÷1 OCCURS分のバイト長＝OCCCURS数
                int occursCount = amounts.length / rec2.getRecordLength();
                // OCCURS数分繰り返す
                for (int i = 0; i < occursCount; i++) {
                    // amountsバイト配列から1 OCCURS分切り出す。
                    rec2.setContents(amounts, i * rec2.getRecordLength());
                    // 1 OCCURSを構成しているORDER-CNOとAMOUNTを標準出力
                    System.out.print("],[" + rec2.getField("ORDER-CNO"));
                    System.out.print("],[" + rec2.getField("AMOUNT"));
                }
                System.out.print("],[" + rec1.getField("ORDER-NOTE"));
                System.out.println("]");
            }
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * AS400Arrayの挙動テスト.
     *
     * AS400Arrayの内訳は、Xタイプ10byte×3 OCCURSの合計30byteのデータ構造。
     * これを、RFMLファイルに保存。XMLにはcount="3"で出力されることを期待。 ここまでは、うまくいった。
     * しかし、RFMLファイルを読み込んでRecordオブジェクトを作って、長さを取得すると、10byte。 OCCURSが再現されない。
     *
     * 結論:RFMLファイルにcountを書き込むことは可能だが、読み込んだ時にcountがなくなるので、RFMLファイルでは、countは使わないこと。
     *
     * @throws XmlException レコードフォーマット例外
     * @throws IOException ファイルIO例外
     */
    @Test
    public void test13() throws IOException, XmlException {
        // 項目定義
        AS400Array as400Array = new AS400Array(new AS400Text(10), 3);
        ArrayFieldDescription afd = new ArrayFieldDescription(as400Array, "ORDER-NOTE");

        // レコードフォーマット定義をファイルで保存。
        RecordFormat rf = new RecordFormat("TEST13-REC");
        rf.addFieldDescription(afd);
        // 期待値:10*3=30byte.
        int expected = rf.getNewRecord().getRecordLength();
        // count="3"で保存される。
        this.saveRecordFormat(rf, new File("src/test/java/jp/golden/cat/util/TEST13.rfml"));

        // RFMLファイルからレコードオブジェクトを生成。
        RecordFormatDocument rfd = this.loadRecordFormatDocument("jp.golden.cat.util.TEST13.rfml");
        // 10*3=30byteを期待しているが、なぜか10が返ってくる。countが無視されている。
        int actual = rfd.toRecordFormat("TEST13-REC").getNewRecord().getRecordLength();
        // afterとして保存してみると、countがなくなって保存される。
        this.saveRecordFormat(rfd.toRecordFormat("TEST13-REC"),
                new File("src/test/java/jp/golden/cat/util/TEST13_after.rfml"));

        System.out.println("test13:actual:" + actual);
        System.out.println("test13:expected:" + expected);
        // assertEquals(expected, actual);
        assertEquals(10, actual);

    }

    /**
     * 注文レコードフォーマット定義作成.
     *
     * @return 注文レコードフォーマット定義作成.
     */
    RecordFormat createOrderRecordFormat() {
        // 項目定義
        BinaryFieldDescription bfd = new BinaryFieldDescription(new AS400Bin4(), "REC-NO");
        CharacterFieldDescription cfd1 = new CharacterFieldDescription(new AS400Text(10), "ORDER-NO");
        PackedDecimalFieldDescription pfd = new PackedDecimalFieldDescription(new AS400PackedDecimal(4, 2), "AMOUNT");
        CharacterFieldDescription cfd2 = new CharacterFieldDescription(new AS400Text(20), "ORDER-NOTE");

        // レコードフォーマット定義
        RecordFormat rf = new RecordFormat("ORDER-REC");
        rf.addFieldDescription(bfd);
        rf.addFieldDescription(cfd1);
        rf.addFieldDescription(pfd);
        rf.addFieldDescription(cfd2);

        return rf;
    }

    /**
     * MFレコードフォーマット定義をクラスパスから取得します。
     *
     * @param rfmlClassPath。
     * @return MFレコードフォーマット定義。
     */
    RecordFormatDocument loadRecordFormatDocument(String rfmlClassPath) {
        RecordFormatDocument rfd = null;
        try {
            rfd = new RecordFormatDocument(rfmlClassPath);
        } catch (XmlException e) {
            throw new RuntimeException(e);
        }
        return rfd;
    }

    /**
     * MFレコードフォーマット定義をファイルとして保存する。
     *
     * @param rf MFレコード定義フォーマット。
     * @param rfml
     *            MFレコードフォーマット定義ファイルパス。クラスパス上に定義ファイルがないと後で、定義ファイルが読み込めないので注意のこと。
     */
    void saveRecordFormat(RecordFormat rf, File rfml) {
        FileOutputStream fos = null;
        try {
            RecordFormatDocument rfd = new RecordFormatDocument(rf);
            fos = new FileOutputStream(rfml);
            rfd.toXml(fos);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * エンディアンの確認.
     *
     * ネイティブバイトオーダー:LITTLE_ENDIAN<br>
     * 初期値:0A0B0C0D12345678<br>
     * LITTLE_ENDIAN:0D0C0B0A BIG_ENDIAN:0A0B0C0D<br>
     * LITTLE_ENDIAN:78563412 BIG_ENDIAN:12345678<br>
     */
    @Test
    public void testEndian() {
        System.out.println("testEndian:");
        // ネイティブのバイトオーダーを標準出力。
        // Javaの内部処理では、ビッグエンディアンであるが、Intel系CPUはリトルエンディアンなので、次のメソッドをIntel系CPUのJavaVMで動かすと、
        // 「ネイティブバイトオーダー:LITTLE_ENDIAN」と表示される。
        System.out.println("ネイティブバイトオーダー:" + ByteOrder.nativeOrder());

        byte[] mfData = new byte[] { 0xA, 0xB, 0xC, 0xD, 0x12, 0x34, 0x56, 0x78 };
        StringUtil.dump("初期値:", mfData, "");

        ByteArrayInputStream bis = null;
        try {
            bis = new ByteArrayInputStream(mfData);

            byte[] buffer = new byte[4];
            int len = -1;
            while ((len = bis.read(buffer)) != -1) {
                if (len != buffer.length) {
                    throw new IOException("読み込みバッファの長さと入力値の関係を見直してください。");
                }
                // リトルエンディアンで変換。
                ByteBuffer byteBuff = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
                int i = byteBuff.getInt();
                System.out.print(ByteOrder.LITTLE_ENDIAN);
                System.out.print(":" + StringUtil.toHexString(BigInteger.valueOf(i).toByteArray()));
                System.out.print("\t");

                // ビッグエンディアンで変換。
                byteBuff = ByteBuffer.wrap(buffer).order(ByteOrder.BIG_ENDIAN);
                i = byteBuff.getInt();
                System.out.print(ByteOrder.BIG_ENDIAN);
                System.out.print(":" + StringUtil.toHexString(BigInteger.valueOf(i).toByteArray()));
                System.out.println();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    ;
                }
            }
        }
    }
}
```