TERASOLUNA Batch Framework for Java (5.x) Development Guideline
TERASOLUNA Batch Framework for Java (5.x) Development Guideline - version 5.7.1.RELEASE, 2026-3-31, commit-id:4e0b014
> INDEX

# ファイルアクセス

## Overview

本節では、ファイルの入出力を行う方法について説明する。

本章における、ファイルのReader/WriterのBean定義については、チャンクモデルとタスクレットモデルとで同じ定義になる。

### 扱えるファイルの種類

扱えるファイルの種類

TERASOLUNA Batch 5.xで扱えるファイルは以下のとおりである。
これは、Spring Batchにて扱えるものと同じである。

- フラットファイル

- XML

ここではフラットファイルの入出力を行うための方法について説明したのち、 XMLについてHow to extendで説明する。

まず、TERASOLUNA Batch 5.xで扱えるフラットファイルの種類を示す。
フラットファイルにおける行をここでは`レコード`と呼び、 ファイルの種類はレコードの形式にもとづく、とする。

表 1. レコード形式
形式 | 概要
--- | ---
可変長レコード | CSVやTSVに代表される区切り文字により各項目を区切ったレコード形式。各項目の長さが可変である。
固定長レコード | 項目の長さ(バイト数)により各項目を区切ったレコード形式。各項目の長さが固定である。
単一文字列レコード | 1レコードを1文字列として扱う形式。

扱えるファイルの構造

フラットファイルの基本構造は以下の2点から構成される。

- レコード区分

- レコードフォーマット

表 2. フラットファイルのフォーマットを構成する要素
要素 | 概要
--- | ---
レコード区分 | レコードの種類、役割を指す。ヘッダ、データ、トレーラなどがある。 詳しくは後述する。
レコードフォーマット | ヘッダ、データ、トレーラレコードがそれぞれ何行あるのか、ヘッダ部～トレーラ部が複数回繰り返されるかなど、レコードの構造を指す。 シングルフォーマットとマルチフォーマットがある。詳しくは後述する。

TERASOLUNA Batch 5.xでは、各種レコード区分をもつシングルフォーマットおよびマルチフォーマットのフラットファイルを扱うことができる。

各種レコード区分およびレコードフォーマットについて説明する。

各種レコード区分の概要を以下に示す。

表 3. レコード区分ごとの特徴
レコード区分 | 概要
--- | ---
ヘッダレコード | ファイル(データ部)の先頭に付与されるレコードである。 フィールド名、ファイル共通の事項、データ部の集計情報などをもつ。
データレコード | ファイルの主な処理対象となるデータをもつレコードである。
トレーラ/フッタレコード | ファイル(データ部)の末尾に付与されるレコードである。 ファイル共通の事項、データ部の集計情報などをもつ。 シングルフォーマットの場合、フッタレコードと呼ばれることもある。
フッタ/エンドレコード | マルチフォーマットの場合にファイルの末尾に付与されるレコードである。 ファイル共通の事項、ファイル全体の集計情報などをもつ。

| レコード区分を示すフィールドについて ヘッダレコードやトレーラレコードをもつフラットファイルでは、レコード区分を示すフィールドをもたせる場合がある。 TERASOLUNA Batch 5.xでは特にマルチフォーマットファイルの処理において、レコード区分ごとに異なる処理を実施する場合などにレコード区分のフィールドを活用する。 レコード区分によって実行する処理を選択する場合の実装は、マルチフォーマットを参考にすること。

| ファイルフォーマット関連の名称について 個々のシステムにおけるファイルフォーマットの定義によっては、 フッタレコードをエンドレコードと呼ぶなど本ガイドラインとは異なる名称が使われている場合がある。 適宜読み替えを行うこと。

シングルフォーマットおよびマルチフォーマットの概要を以下に示す。

表 4. シングルフォーマットおよびマルチフォーマットの概要
フォーマット | 概要
--- | ---
シングルフォーマット | ヘッダn行 + データn行 + トレーラn行 の形式である。
マルチフォーマット | (ヘッダn行 + データn行 + トレーラn行)* n + フッタn行 の形式である。 シングルフォーマットを複数回繰り返した後にフッタレコードが付与されている形式である。

マルチフォーマットのレコード構成を図に表すと下記のようになる。

図 1. マルチフォーマットのレコード構成図

シングルフォーマット、マルチフォーマットフラットファイルの例を以下に示す。
なお、ファイルの内容説明に用いるコメントアウトを示す文字として`//`を使用する。

シングルフォーマット、レコード区分なしフラットファイル(CSV形式)の例

    branchId,year,month,customerId,amount  // (1)
    000001,2016,1,0000000001,100000000  // (2)
    000001,2016,1,0000000002,200000000  // (2)
    000001,2016,1,0000000003,300000000  // (2)
    000001,3,600000000  // (3)

表 5. ファイルの内容の項目一覧
項番 | 説明
--- | ---
(1) | ヘッダレコードである。 データ部のフィールド名を示している。
(2) | データレコードである。
(3) | トレーラレコードである。 データ部の集計情報を保持している。

マルチフォーマット、レコード区分ありのフラットファイル(CSV形式)の例

    // (1)
    H,branchId,year,month,customerId,amount  // (2)
    D,000001,2016,1,0000000001,100000000
    D,000001,2016,1,0000000002,200000000
    D,000001,2016,1,0000000003,300000000
    T,000001,3,600000000
    H,branchId,year,month,customerId,amount  // (2)
    D,00002,2016,1,0000000004,400000000
    D,00002,2016,1,0000000005,500000000
    D,00002,2016,1,0000000006,600000000
    T,00002,3,1500000000
    H,branchId,year,month,customerId,amount  // (2)
    D,00003,2016,1,0000000007,700000000
    D,00003,2016,1,0000000008,800000000
    D,00003,2016,1,0000000009,900000000
    T,00003,3,2400000000
    F,3,9,4500000000  // (3)

表 6. ファイルの内容の項目一覧
項番 | 説明
--- | ---
(1) | レコードの先頭にレコード区分を示すフィールドをもっている。 それぞれ下記のレコード区分を示す。 `H`：ヘッダレコード `D`：データレコード `T`：トレーラレコード `F`：フッタレコード
(2) | branchIdが変わるごとにヘッダ、データ、トレーラを3回繰り返している。
(3) | フッタレコードである。 ファイル全体の集計情報を保持している。

| データ部のフォーマットに関する前提 How to useでは、データ部のレコードが同一のフォーマットである事を前提として説明する。 これは、データ部のレコードがすべて同じ変換対象クラスへマッピングされることを意味する。

| マルチフォーマットファイルの説明について How to useでは、シングルフォーマットファイルについて説明する。 マルチフォーマットや上記の構造にフッタ部を含む構造をもつフラットファイルについては、How to extendを参照。

### フラットファイルの入出力を行うコンポーネント

フラットファイルを扱うためのクラスを示す。

入力

フラットファイルの入力を行うために使用するクラスの関連は以下のとおりである。

図 2. フラットファイルの入力を行うために使用するクラスの関連

各コンポーネントの呼び出し関係は以下のとおりである。

図 3. 各コンポーネントの呼び出し関係

各コンポーネントの詳細を以下に示す。

org.springframework.batch.item.file.FlatFileItemReader

  フラットファイルを読み込みに使用する`ItemReader`の実装クラス。以下のコンポーネントを利用する。
  簡単な処理の流れは以下のとおり。
  1.`BufferedReaderFactory`を使用して`BufferedReader`を取得する。
  2.取得した`BufferedReader`を使用してフラットファイルから1レコードを読み込む。
  3.`LineMapper`を使用して1レコードを対象Beanへマッピングする。

  org.springframework.batch.item.file.BufferedReaderFactory

    ファイルを読み込むための`BufferedReader`を生成する。

  org.springframework.batch.item.file.LineMapper

    1レコードを対象Beanへマッピングする。以下のコンポーネントを利用する。
    簡単な処理の流れは以下のとおり。
    1.`LineTokenizer`を使用して1レコードを各項目に分割する。
    2.`FieldSetMapper`によって分割した項目をBeanのプロパティにマッピングする。

    org.springframework.batch.item.file.transform.LineTokenizer

      ファイルから取得した1レコードを各項目に分割する。
      分割された各項目は`FieldSet`クラスに格納される。

    org.springframework.batch.item.file.mapping.FieldSetMapper

      分割した1レコード内の各項目を対象Beanのプロパティへマッピングする。

出力

フラットファイルの出力を行うために使用するクラスの関連は以下のとおりである。

図 4. フラットファイルの出力を行うために使用するクラスの関連

各コンポーネントの呼び出し関係は以下のとおりである。

図 5. 各コンポーネントの呼び出し関係

org.springframework.batch.item.file.FlatFileItemWriter

  フラットファイルへの書き出しに使用する`ItemWriter`の実装クラス。以下のコンポーネントを利用する。 `LineAggregator`対象Beanを1レコードへマッピングする。

  org.springframework.batch.item.file.transform.LineAggregator

    対象Beanを1レコードへマッピングするために使う。 Beanのプロパティとレコード内の各項目とのマッピングは`FieldExtractor`で行う。

    org.springframework.batch.item.file.transform.FieldExtractor

      対象Beanのプロパティを1レコード内の各項目へマッピングする。

## How to use

フラットファイルのレコード形式別に使い方を説明する。

- 可変長レコード

- 固定長レコード

- 単一文字列レコード

その後、以下の項目について説明する。

- ヘッダとフッタ

- 複数ファイル

- コントロールブレイク

### 可変長レコード

可変長レコードファイルを扱う場合の定義方法を説明する。

#### 入力

下記の入力ファイルを読み込むための設定例を示す。

入力ファイル例

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義例

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(); // (5)
        tokenizer.setNames("branchId", "year", "month", "customerId", "amount"); // (6)
        tokenizer.setDelimiter(","); // (7)
        tokenizer.setQuoteCharacter('"'); // (8)
        final BeanWrapperFieldSetMapper<SalesPlanDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>(); // (9)
        fieldSetMapper.setTargetType(SalesPlanDetail.class);
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>(); // (4)
        lineMapper.setLineTokenizer(tokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemReader.class))
                .resource(new FileSystemResource(inputFile)) // (1)
                .lineMapper(lineMapper)
                .encoding("MS932") // (2)
                .strict(true) // (3)
                .build();
    }

`FlatFileItemReaderBuilder`クラスでの`name`メソッドの使用

`FlatFileItemReaderBuilder`は、`FlatFileItemReader`を生成するBuilderクラスである。

本章で登場する以下のBuilderクラスでは、デフォルトで`name`メソッドの使用が必須となっている。

- `FlatFileItemReaderBuilder` / `FlatFileItemWriterBuilder`

- `MultiResourceItemReaderBuilder` / `MultiResourceItemWriterBuilder`

- `StaxEventItemReaderBuilder` / `StaxEventItemWriterBuilder`

`name`メソッドでは、`ExecutionContext`に実行状態を保存する際のキー名を引数に設定する。キー名は、特段の理由がないかぎり、`ItemStreamSupport`のサブクラス名を指定する。`ItemStreamSupport`のサブクラスとは、Builderクラスで生成するクラスのことである。

`name`メソッドで`ItemStreamSupport`のサブクラス名を指定する理由

`ExecutionContext`やログに出力される名前を、その出力元のクラス名と一致させておくことで、トレーサビリティを担保する。これは障害解析時等に、原因箇所特定の手がかりとするためである。

FlatFileItemReaderBuilderでの設定例

`ItemStreamSupport`のサブクラス名は`org.springframework.batch.item.file.FlatFileItemReader`の短縮名、すなわち`"FlatFileItemReader"`となる。下記のように設定する。

    return new FlatFileItemReaderBuilder<SalesPlanDetail>()
      .name(ClassUtils.getShortName(FlatFileItemReader.class))

なお、Builderクラスを使用せず直接`FlatFileItemReader`をnewしたときも、デフォルトでコンストラクタ内で`ItemStreamSupport`のサブクラス名（`"FlatFileItemReader"`）が設定される（以下）。

org.springframework.batch.item.file.FlatFileItemReaderのコンストラクタ

    public FlatFileItemReader() {
      setName(ClassUtils.getShortName(FlatFileItemReader.class));
    }

`name`メソッドについては以下の公式リファレンスを参照のこと。

- クラス FlatFileItemReaderBuilder<T>

- クラス FlatFileItemWriterBuilder<T>

- クラス MultiResourceItemReaderBuilder<T>

- クラス MultiResourceItemWriterBuilder<T>

- クラス StaxEventItemReaderBuilder<T>

- クラス StaxEventItemWriterBuilder<T>

XMLConfig
Bean定義例

    <!-- (1) (2) (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="MS932"
          p:strict="true">
      <property name="lineMapper">  <!-- (4) -->
        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
          <property name="lineTokenizer">  <!-- (5) -->
            <!-- (6) (7) (8) -->
            <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                  p:names="branchId,year,month,customerId,amount"
                  p:delimiter=","
                  p:quoteCharacter='"'/>
          </property>
          <property name="fieldSetMapper">  <!-- (9) -->
            <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                  p:targetType="org.terasoluna.batch.functionaltest.app.model.plan.SalesPlanDetail"/>
          </property>
        </bean>
      </property>
    </bean>

表 7. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(4) | lineMapper | `org.springframework.batch.item.file.mapping.DefaultLineMapper`を設定する。 `DefaultLineMapper`は、設定された`LineTokenizer`と`FieldSetMapper`を用いてレコードを変換対象クラスへ変換する基本的な動作を提供する`LineMapper`である。 |  | なし
(5) | lineTokenizer | `org.springframework.batch.item.file.transform.DelimitedLineTokenizer`を設定する。 `DelimitedLineTokenizer`は、区切り文字を指定してレコードを分割する`LineTokenizer`の実装クラス。 CSV形式の一般的書式とされるRFC-4180の仕様に定義されている、エスケープされた改行、区切り文字、囲み文字の読み込みに対応している。 |  | なし
(6) | names | 1レコードの各項目に名前を付与する。 `FieldSetMapper`で使われる`FieldSet`で設定した名前を用いて各項目を取り出すことができるようになる。 レコードの先頭から各名前をカンマ区切りで設定する。 `BeanWrapperFieldSetMapper`を利用する場合は、必須設定である。 |  | なし
(7) | delimiter | 区切り文字を設定する。 TSVファイルの読み込みを行う場合には、TSV形式ファイルの入力方法を参照。 |  | カンマ
(8) | quoteCharacter | 囲み文字を設定する |  | なし
(9) | fieldSetMapper | 文字列や数字など特別な変換処理が不要な場合は、`org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper`を利用し、 `targetType`属性に変換対象クラスを指定する。 これにより、(5)で設定した各項目の名前と一致するフィールドに値を自動的に設定したインスタンスを生成する。 変換処理が必要な場合は、`org.springframework.batch.item.file.mapping.FieldSetMapper`の実装クラスを設定する。 |  | なし

TSV形式ファイルの入力方法

TSVファイルの読み込みを行う場合には、区切り文字にタブを設定することで実現可能である。

JavaConfig
TSVファイル読み込み時:区切り文字設定例(定数による設定)

    tokenizer.setDelimiter(DelimitedLineTokenizer.DELIMITER_TAB);

XMLConfig
TSVファイル読み込み時:区切り文字設定例(定数による設定)

    <property name="delimiter">
        <util:constant
                static-field="org.springframework.batch.item.file.transform.DelimitedLineTokenizer.DELIMITER_TAB"/>
    </property>

| FieldSetMapperの独自実装について FieldSetMapperを独自に実装する場合については、How to extendを参照。

| BeanWrapperFieldSetMapperの留意事項 `BeanWrapperFieldSetMapper`は、文字列を`trim`するため、先頭・末尾の空白と制御文字が削除されることに留意すること。

#### 出力

下記の出力ファイルを書き出すための設定例を示す。

出力ファイル例

    001,CustomerName001,CustomerAddress001,11111111111,001
    002,CustomerName002,CustomerAddress002,11111111111,002
    003,CustomerName003,CustomerAddress003,11111111111,003

変換対象クラス

    public class Customer {

        private String customerId;
        private String customerName;
        private String customerAddress;
        private String customerTel;
        private String chargeBranchId;
        private LocalDateTime createDate;
        private LocalDateTime updateDate;

        // omitted getter/setter
    }

上記のファイルを書き出すための設定は以下のとおり。

JavaConfig
Bean定義例

    // Writer
    @Bean
    @StepScope
    public FlatFileItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        final BeanWrapperFieldExtractor<Customer> fieldExtractor = new BeanWrapperFieldExtractor<>(); // (10)
        fieldExtractor.setNames(
                new String[] { "customerId", "customerName", "customerAddress",
                        "customerTel", "chargeBranchId" }); // (11)
        final DelimitedLineAggregator<Customer> lineAggregator = new DelimitedLineAggregator<>(); // (8)
        lineAggregator.setDelimiter(","); // (9)
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .lineSeparator("\n") // (3)
                .append(true) // (4)
                .shouldDeleteIfExists(false) // (5)
                .shouldDeleteIfEmpty(false) // (6)
                .transactional(true) // (7)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義例

    <!-- Writer -->
    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:lineSeparator="&#x0A;"
          p:appendAllowed="true"
          p:shouldDeleteIfExists="false"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true">
      <property name="lineAggregator">  <!-- (8) -->
        <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"
              p:delimiter=",">  <!-- (9) -->
          <property name="fieldExtractor">  <!-- (10) -->
            <!-- (11) -->
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="customerId,customerName,customerAddress,customerTel,chargeBranchId"/>
          </property>
        </bean>
      </property>
    </bean>

表 8. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | lineSeparator | レコード区切り(改行コード)を設定する。 |  | システムプロパティの`line.separator`
(4) | appendAllowed | trueの場合、既存のファイルに追記をする。 trueの場合、shouldDeleteIfExistsの設定値は無効化されるため、注意が必要である。 |  | false
(5) | shouldDeleteIfExists | appendAllowedがtrueの場合は、この設定は無効化されるため、値を指定しないことを推奨する。 ファイル出力前に出力ファイルが存在していた場合の動作を設定する。 trueの場合、ファイル出力前に既にファイルが存在すれば削除する。 falseの場合、ファイル出力前に既にファイルが存在すれば例外をスローする。 |  | true
(6) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(7) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(8) | lineAggregator | `org.springframework.batch.item.file.transform.DelimitedLineAggregator`を設定する。 フィールドを囲み文字で囲む場合は、`org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator`を設定する。 `EnclosableDelimitedLineAggregator`の使用方法は後述する。 |  | なし
(9) | delimiter | 区切り文字を設定する。 |  | カンマ
(10) | fieldExtractor | 文字列や数字など特別な変換処理が不要な場合は、`org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor`が利用できる。 変換処理が必要な場合は、`org.springframework.batch.item.file.transform.FieldExtractor`の実装クラスを設定する。 `FieldExtractor`の実装例は固定長レコードの出力にて、全角文字のフォーマットを例に説明しているためそちらを参照。 |  | なし
(11) | names | 1レコードの各項目に名前を付与する。 レコードの先頭から各名前をカンマ区切りで設定する。 |  | なし

EnclosableDelimitedLineAggregatorについて

フィールドを囲み文字で囲む場合は、TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator`を使用する。
この拡張クラスでは、Spring Batchにより提供されている`org.springframework.batch.item.file.transform.DelimitedLineAggregator`に、フィールドを囲み文字で囲む処理に対応することができ、RFC-4180 で求められている要件を満たすための拡張が行われている。 本機能の実装の経緯については、 Spring Batch/BATCH-2463 を参照。
`EnclosableDelimitedLineAggregator`の仕様は以下のとおり。

- 囲み文字、区切り文字を任意に指定可能

  - デフォルトはCSV形式で一般的に使用される以下の値である

    - 囲み文字：`"`(ダブルクォート)

    - 区切り文字：`,`(カンマ)

- CSV形式のフォーマットについて、CSV形式の一般的書式とされるRFC-4180では下記のように定義されている。

  - フィールドに改行、囲み文字、区切り文字が含まれていない場合、各フィールドはダブルクォート(囲み文字)で囲んでも囲わなくてもよい

  - 改行(CRLF)、ダブルクォート(囲み文字)、カンマ(区切り文字)を含むフィールドは、ダブルクォートで囲むべきである

  - フィールドがダブルクォート(囲み文字)で囲まれている場合、フィールドの値に含まれるダブルクォートは、その直前に1つダブルクォートを付加して、エスケープしなければならない

`EnclosableDelimitedLineAggregator`の使用方法を以下に示す。

出力ファイル例

    "001","CustomerName""001""","CustomerAddress,001","11111111111","001"
    "002","CustomerName""002""","CustomerAddress,002","11111111111","002"
    "003","CustomerName""003""","CustomerAddress,003","11111111111","003"

変換対象クラス

    // 上記の例と同様

JavaConfig
Bean定義例(lineAggregatorの設定のみ)

    final EnclosableDelimitedLineAggregator<SalesPlanDetail> lineAggregator = new EnclosableDelimitedLineAggregator<>(); // (1)
    lineAggregator.setDelimiter(','); // (2)
    lineAggregator.setEnclosure('"'); // (3)
    lineAggregator.setAllEnclosing(true); // (4)
    lineAggregator.setFieldExtractor(fieldExtractor);

XMLConfig
Bean定義例(lineAggregatorの設定のみ)

    <property name="lineAggregator">  <!-- (1) -->
      <!-- (2) (3) (4) -->
      <bean class="org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator"
            p:delimiter=","
            p:enclosure='"'
            p:allEnclosing="true">
          <property name="fieldExtractor">
            <!-- omitted settings -->
          </property>
      </bean>
    </property>

表 9. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | lineAggregator | `org.terasoluna.batch.item.file.transform.EnclosableDelimitedLineAggregator`を設定する。 `EnclosableDelimitedLineAggregator`の使用方法はEnclosableDelimitedLineAggregatorについてを参照。 |  | なし
(2) | delimiter | 区切り文字を設定する。 TSVファイルの出力を行う場合には、TSV形式ファイルの出力方法を参照。 |  | カンマ
(3) | enclosure | 囲み文字を設定する。 囲み文字がフィールドに含まれる場合は、エスケープ処理として囲み文字を2つ連結されたものへ置換される。 |  | ダブルクォート
(4) | allEnclosing | trueの場合、すべてのフィールドが囲み文字で囲まれる。 falseの場合フィールド内に行頭復帰(CR)、改行(LF)、区切り文字、囲み文字が含まれるフィールドのみ囲み文字で囲まれる。 |  | false

TSV形式ファイルの出力方法

TSVファイルの出力を行う場合には、区切り文字にタブを設定することで実現可能である。

JavaConfig
TSVファイル出力時の区切り文字設定例(定数による設定)

    tokenizer.setDelimiter(DelimitedLineTokenizer.DELIMITER_TAB);

XMLConfig
TSVファイル出力時の区切り文字設定例(定数による設定)

    <property name="delimiter">
        <util:constant
                static-field="org.springframework.batch.item.file.transform.DelimitedLineTokenizer.DELIMITER_TAB"/>
    </property>

#### FlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違い

`FlatFileItemWriter`のプロパティである、`shouldDeleteIfExists`と`shouldDeleteIfEmpty`は実行タイミングや判定対象が異なる。

表 10. shouldDeleteIfExistsとshouldDeleteIfEmptyの違い
| 機能 | 判定タイミング | 判定対象 | 設定値(true/false)の挙動
--- | --- | --- | --- | ---
shouldDeleteIfExists | 同一名称の出力ファイルが既に存在する場合の挙動を制御する | ファイル出力前 | 同一名称の出力ファイルの有無 | true:既存の出力ファイルを削除し、新たに出力ファイルを作成する false:例外をスローする
shouldDeleteIfEmpty | ジョブの出力件数が0件の場合の挙動を制御する | ファイル出力後 | ジョブの出力件数が0件かどうか (*1) | true:出力ファイルを削除する false:何もしない

*1. 特定条件下では出力件数が0件ではない場合でもファイルが削除される場合がある、詳細は後述の「`shouldDeleteIfEmpty`がtrueの場合の注意すべき動作」を参照。

`shouldDeleteIfExists`はファイル出力前の出力ファイルを対象とし、`shouldDeleteIfEmpty`はファイル出力後の出力件数を対象をしている。

`shouldDeleteIfEmpty`がtrueの場合の注意すべき動作について説明する。
以下の条件を満たした場合、出力件数が0件でなくとも出力ファイルが削除されるため注意が必要である。

- `shouldDeleteIfExists`にfalseを設定

- `shouldDeleteIfEmpty`にtrueを設定

- Jobによるファイル出力前に既に出力ファイルが存在する

条件を満たした場合、以下の様に処理が進む。

1. `shouldDeleteIfExists`はジョブ開始時に既存ファイルがあるかチェックされる。

2. この時点ではまだ`ItemWriter`によるファイル出力前だが、同名のファイルが既に存在したときはファイル出力を行われずに例外がスローされる。

3. しかし例外スロー前に後処理としてファイルクローズが行われ、ジョブ終了時の`shouldDeleteIfEmpty`が評価される。

4. ここで`shouldDeleteIfEmpty`がtrueの場合、前処理で例外がスローされているためジョブの出力件数が0件となりファイルが削除される。
   これは既存ファイルのレコードがたとえ10000行あっても出力件数が0件として評価されファイルが削除される。

このように、`shouldDeleteIfExists`と`souldDeleteIfEmpty`の設定値の組み合わせによっては、出力ファイルが意図せず削除される恐れがあり注意が必要である。
意図せぬ出力ファイルの削除を避けるため、以下を検討するとよい。

- 不要な出力ファイルは、別の方法で削除する

  - ジョブスケジューラ等で、不要な出力ファイルを定期的にクリーンアップする

- `shouldDeleteIfExists`、`shouldDeleteIfEmpty`による出力ファイル削除を禁止する

  - `shouldDeleteIfExists`、`shouldDeleteIfEmpty`はともにfalseとする

以上のことから、`shouldDeleteIfExists`と`shouldDeleteIfEmpty`は実行タイミングや判定対象が異なることと、特定条件下で出力ファイルが意図せず削除される恐れがあるため注意が必要である。

### 固定長レコード

固定長レコードファイルを扱う場合の定義方法を説明する。

#### 入力

下記の入力ファイルを読み込むための設定例を示す。

TERASOLUNA Batch 5.xでは、レコードの区切りを改行で判断する形式とバイト数で判断する形式 に対応している。

入力ファイル例1(レコードの区切りは改行)

    売上012016 1   00000011000000000
    売上022017 2   00000022000000000
    売上032018 3   00000033000000000

入力ファイル例2(レコードの区切りはバイト数、32バイトで1レコード)

    売上012016 1   00000011000000000売上022017 2   00000022000000000売上032018 3   00000033000000000

表 11. 入力ファイル仕様
項番 | フィールド名 | データ型 | バイト数
--- | --- | --- | ---
(1) | branchId | String | 6
(2) | year | int | 4
(3) | month | int | 2
(4) | customerId | String | 10
(5) | amount | BigDecimal | 10

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義例

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") Resource inputFile) {
        BufferedReaderFactory  bufferedReaderFactory = new DefaultBufferedReaderFactory(); // (4)
        Range[] ranges = new Range[] {new Range(1, 6), new Range(7, 10), new Range(11, 12), new Range(13, 22), new Range(23, 32)}; // (8)
        final FixedByteLengthLineTokenizer tokenizer = new FixedByteLengthLineTokenizer(Charset.forName("MS932"), ranges); // (6)(9)
        tokenizer.setNames("branchId", "year", "month", "customerId", "amount"); // (7)
        final BeanWrapperFieldSetMapper<SalesPlanDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>(); // (10)
        fieldSetMapper.setTargetType(SalesPlanDetail.class);
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>(); // (5)
        lineMapper.setLineTokenizer(tokenizer); // (6)
        lineMapper.setFieldSetMapper(fieldSetMapper);
        FileSystemResourceLoader loader = new FileSystemResourceLoader();
        return new FlatFileItemReaderBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemReader.class))
                .resource(inputFile) // (1)
                .encoding("MS932") // (2)
                .strict(true) // (3)
                .bufferedReaderFactory(bufferedReaderFactory) // (4)
                .lineMapper(lineMapper) // (5)
                .build();
    }

XMLConfig
Bean定義例

    <!-- (1) (2) (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="MS932"
          p:strict="true">
        <property name="bufferedReaderFactory">  <!-- (4) -->
            <bean class="org.springframework.batch.item.file.DefaultBufferedReaderFactory"/>
        </property>
        <property name="lineMapper">  <!-- (5) -->
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">  <!-- (6) -->
                    <!-- (7) -->
                    <!-- (8) -->
                    <!-- (9) -->
                    <bean class="org.terasoluna.batch.item.file.transform.FixedByteLengthLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"
                          c:ranges="1-6, 7-10, 11-12, 13-22, 23-32"
                          c:charset="MS932" />
                </property>
                <property name="fieldSetMapper">  <!-- (10) -->
                  <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                        p:targetType="org.terasoluna.batch.functionaltest.app.model.plan.SalesPlanDetail"/>
                </property>
            </bean>
        </property>
    </bean>

表 12. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(4) | bufferedReaderFactory | レコードの区切りを改行で判断する場合は、デフォルト値である`org.springframework.batch.item.file.DefaultBufferedReaderFactory`を使用する。 `DefaultBufferedReaderFactory`が生成する`BufferedReader`は改行までを1レコードとして取得する。 レコードの区切りをバイト数で判断する場合は、TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.FixedByteLengthBufferedReaderFactory`を設定する。 `FixedByteLengthBufferedReaderFactory`が生成する`BufferedReader`は指定したバイト数までを1レコードとして取得する。 `FixedByteLengthBufferedReaderFactory`の詳しい仕様および使用方法は後述する。 |  | `DefaultBufferedReaderFactory`
(5) | lineMapper | `org.springframework.batch.item.file.mapping.DefaultLineMapper`を設定する。 |  | なし
(6) | lineTokenizer | TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.transform.FixedByteLengthLineTokenizer`を設定する。 |  | なし
(7) | names | 1レコードの各項目に名前を付与する。 `FieldSetMapper`で使われる`FieldSet`で設定した名前を用いて各項目を取り出すことができるようになる。 レコードの先頭から各名前をカンマ区切りで設定する。 `BeanWrapperFieldSetMapper`を利用する場合は、必須設定である。 |  | なし
(8) | ranges (コンストラクタ引数) | 区切り位置を設定する。レコードの先頭から区切り位置をカンマ区切りで設定する。 各区切り位置の単位はバイトであり、`開始位置,終了位置(JavaConfig)/開始位置-終了位置(XMLConfig)`形式で指定する。 区切り位置を設定した順番でレコードから指定された範囲を取得し、`FieldSet`に格納される。 (6)のnamesを指定した場合は区切り位置を設定した順番でnamesと対応付けて`FieldSet`に格納される。 |  | なし
(9) | charset (コンストラクタ引数) | (2)で指定したエンコーディングと同じ値を設定する。 |  | なし
(10) | fieldSetMapper | 文字列や数字など特別な変換処理が不要な場合は、`org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper`を利用し、 `targetType`属性に変換対象クラスを指定する。 これにより、(6)で設定した各項目の名前と一致するフィールドに値を自動的に設定したインスタンスを生成する。 変換処理が必要な場合は、`org.springframework.batch.item.file.mapping.FieldSetMapper`の実装クラスを設定する。 |  | なし

| FieldSetMapperの独自実装について FieldSetMapperを独自に実装する場合については、How to extendを参照。

FixedByteLengthBufferedReaderFactoryの使用方法

レコードの区切りをバイト数で判断するファイルを読み込む場合は、TERASOLUNA Batch 5.xが提供する`org.terasoluna.batch.item.file.FixedByteLengthBufferedReaderFactory`を使用する。

`FixedByteLengthBufferedReaderFactory`を使用することで指定したバイト数までを1レコードとして取得することができる。
`FixedByteLengthBufferedReaderFactory`の仕様は以下のとおり。

- コンストラクタ引数としてレコードのバイト数を指定する

- 指定されたバイト数を1レコードとしてファイルを読み込む`FixedByteLengthBufferedReader`を生成する

`FixedByteLengthBufferedReader`の使用は以下のとおり。

- インスタンス生成時に指定されたバイト長を1レコードとしてファイルを読み込む

- 改行コードが存在する場合、破棄せず1レコードのバイト長に含めて読み込みを行う

- 読み込み時に使用するファイルエンコーディングは`FlatFileItemWriter`に設定したものが`BufferedReader`生成時に設定される

`FixedByteLengthBufferedReaderFactory`の定義方法を以下に示す。

JavaConfig

    FixedByteLengthBufferedReaderFactory bufferedReaderFactory = new FixedByteLengthBufferedReaderFactory(32); // (1)

XMLConfig

    <property name="bufferedReaderFactory">
        <bean class="org.terasoluna.batch.item.file.FixedByteLengthBufferedReaderFactory"
            c:byteLength="32"/>  <!-- (1) -->
    </property>

表 13. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | byteLength (コンストラクタ引数) | 1レコードあたりのバイト数を設定する。 |  | なし

| 固定長ファイルを扱う場合に使用するコンポーネント 固定長ファイルを扱う場合は、TERASOLUNA Batch 5.xが提供するコンポーネントを使うことを前提にしている。 FixedByteLengthBufferedReaderFactory 改行なし固定長ファイルから、指定したエンコーディングのバイト数で1レコードを読み込む`BufferedReader`生成クラス FixedByteLengthLineTokenizer マルチバイト文字列に対応したバイト数区切りの`FixedLengthTokenizer`拡張クラス

| マルチバイト文字列を含むレコードを処理する場合 マルチバイト文字列を含むレコードを処理する場合は、`FixedByteLengthLineTokenizer`を必ず利用する。 Spring Batchが提供する`FixedLengthTokenizer`は、レコードをバイト数ではなく文字数で区切ってしまうため、期待どおりの項目切り出しが行われない恐れがある。 この点についてはSpring Batch/BATCH-2540で報告しているため、今後不要になる可能性がある。

| FieldSetMapperの実装については、How to extendを参照。

#### 出力

下記の出力ファイルを書き出すための設定例を示す。

固定長ファイルを書き出すためには、Beanから取得した値をフィールドのバイト数にあわせてフォーマットを行う必要がある。
フォーマットの実行方法は全角文字が含まれるか否かによって下記のように異なる。

- 全角文字が含まれない場合(半角文字のみであり文字のバイト数が一定)

  - `FormatterLineAggregator`にてフォーマットを行う。

  - フォーマットは、`String.format`メソッドで使用する書式で設定する。

- 全角文字が含まれる場合(文字コードによって文字のバイト数が一定ではない)

  - `FieldExtractor`の実装クラスにてフォーマットを行う。

まず、出力ファイルに全角文字が含まれない場合の設定例を示し、その後全角文字が含まれる場合の設定例を示す。

出力ファイルに全角文字が含まれない場合の設定について下記に示す。

出力ファイル例

       0012016 10000000001  10000000
       0022017 20000000002  20000000
       0032018 30000000003  30000000

表 14. 出力ファイル仕様
項番 | フィールド名 | データ型 | バイト数
--- | --- | --- | ---
(1) | branchId | String | 6
(2) | year | int | 4
(3) | month | int | 2
(4) | customerId | String | 10
(5) | amount | BigDecimal | 10

フィールドのバイト数に満たない部分は半角スペース埋めとしている。

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

上記のファイルを書き出すための設定は以下のとおり。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        final BeanWrapperFieldExtractor<SalesPlanDetail> fieldExtractor = new BeanWrapperFieldExtractor<>(); // (10)
        fieldExtractor.setNames(
                new String[] { "branchId", "year", "month", "customerId", "amount" });
        final FormatterLineAggregator<SalesPlanDetail> lineAggregator = new FormatterLineAggregator<>(); // (8)
        lineAggregator.setFormat("%6s%4s%2s%10s%10s"); // (9)
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .lineSeparator("\n") // (3)
                .append(true) // (4)
                .shouldDeleteIfExists(false) // (5)
                .shouldDeleteIfEmpty(false) // (6)
                .transactional(true) // (7)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <!-- Writer -->
    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:lineSeparator="&#x0A;"
          p:appendAllowed="true"
          p:shouldDeleteIfExists="false"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true">
        <property name="lineAggregator">  <!-- (8) -->
            <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator"
                  p:format="%6s%4s%2s%10s%10s">  <!-- (9) -->
                <property name="fieldExtractor">  <!-- (10) -->
                  <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                        p:names="branchId,year,month,customerId,amount"/>  <!-- (11) -->
                </property>
            </bean>
        </property>
    </bean>

表 15. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | lineSeparator | レコード区切り(改行コード)を設定する。 改行なしにする場合は、空文字を設定する。 |  | システムプロパティの`line.separator`
(4) | appendAllowed | trueの場合、既存のファイルに追記をする。 trueの場合、shouldDeleteIfExistsの設定値は無効化されるため、注意が必要である。 |  | false
(5) | shouldDeleteIfExists | appendAllowedがtrueの場合は、この設定は無効化されるため、値を指定しないことを推奨する。 ファイル出力前に出力ファイルが存在していた場合の動作を設定する。 trueの場合、ファイル出力前に既にファイルが存在すれば削除する。 falseの場合、ファイル出力前に既にファイルが存在すれば例外をスローする。 |  | true
(6) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(7) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(8) | lineAggregator | `org.springframework.batch.item.file.transform.FormatterLineAggregator`を設定する。 |  | なし
(9) | format | `String.format`メソッドで使用する書式で出力フォーマットを設定する。 |  | なし
(10) | fieldExtractor | 文字列や数字など特別な変換処理、全角文字のフォーマットが不要な場合は、`org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor`が利用できる。 値の変換処理や全角文字をフォーマットする等の対応が必要な場合は、`org.springframework.batch.item.file.transform.FieldExtractor`の実装クラスを設定する。 全角文字をフォーマットする場合における`FieldExtractor`の実装例は後述する。 |  | `PassThroughFieldExtractor`
(11) | names | 1レコードの各項目に名前を付与する。 レコードの先頭から各フィールドの名前をカンマ区切りで設定する。 |  | なし

| PassThroughFieldExtractorとは `FormatterLineAggregator`がもつ`fieldExtractor`プロパティのデフォルト値は`org.springframework.batch.item.file.transform.PassThroughFieldExtractor`である。 `PassThroughFieldExtractor`は、もとのアイテムに対して処理を行わずに返すクラスであり、`FieldExtractor`にて何も処理を行わない場合に使用する。 アイテムが配列またはコレクションの場合はそのまま返されるが、それ以外の場合は、単一要素の配列にラップされる。

全角文字が含まれるフィールドに対してフォーマットを行う際の設定例

全角文字に対するフォーマットを行う場合、文字コードにより1文字あたりのバイト数が異なるため、`FormatterLineAggregator`ではなく、`FieldExtractor`の実装クラスを使用する。

`FieldExtractor`の実装クラスは以下の要領で実装する。

- `FieldExtractor`クラスを実装し、`extract`メソッドをオーバーライドする

- `extract`メソッドは以下の要領で実装する

  - item(処理対象のBean)から値を取得し、適宜変換処理等を行う

  - Object型の配列に格納し返す

`FieldExtractor`の実装クラスで行う全角文字を含むフィールドのフォーマットは以下の要領で実装する。

- 文字コードに対するバイト数を取得する

- 取得したバイト数をもとにパディング・トリム処理で整形する

以下に全角文字を含むフィールドをフォーマットする場合の設定例を示す。

出力ファイル例

       0012016 10000000001  10000000
      番号2017 2 売上高002  20000000
     番号32018 3   売上003  30000000

出力ファイルの使用は上記の例と同様。

JavaConfig
Bean定義(lineAggregatorの設定のみ)

    final FormatterLineAggregator<SalesPlanDetail> lineAggregator = new FormatterLineAggregator<>(); // (1)
    lineAggregator.setFormat("%s%4s%2s%s%10s"); // (2)
    final SalesPlanFixedLengthFieldExtractor fieldExtractor = new SalesPlanFixedLengthFieldExtractor(); // (3)
    lineAggregator.setFieldExtractor(fieldExtractor);

XMLConfig
Bean定義(lineAggregatorの設定のみ)

    <property name="lineAggregator">  <!-- (1) -->
        <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator"
              p:format="%s%4s%2s%s%10s">  <!-- (2) -->
            <property name="fieldExtractor">  <!-- (3) -->
                <bean class="org.terasoluna.batch.functionaltest.ch05.fileaccess.plan.SalesPlanFixedLengthFieldExtractor"/>
            </property>
        </bean>
    </property>

表 16. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | lineAggregator | `org.springframework.batch.item.file.transform.FormatterLineAggregator`を設定する。 |  | なし
(2) | format | `String.format`メソッドで使用する書式で出力フォーマットを設定する。 全角文字が含まれないフィールドに対してのみ桁数の指定をしている。 |  | なし
(3) | fieldExtractor | `FieldExtractor`の実装クラスを設定する。 実装例は後述する。 |  | `PassThroughFieldExtractor`

変換対象クラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

全角文字をフォーマットするFieldExtractorの実装例

    public class SalesPlanFixedLengthFieldExtractor implements FieldExtractor<SalesPlanDetail> {
        // (1)
        @Override
        public Object[] extract(SalesPlanDetail item) {
            Object[] values = new Object[5];  // (2)

            // (3)
            values[0] = fillUpSpace(item.getBranchId(), 6);  // (4)
            values[1] = item.getYear();
            values[2] = item.getMonth();
            values[3] = fillUpSpace(item.getCustomerId(), 10);  // (4)
            values[4] = item.getAmount();

            return values; // (8)
        }

        // It is a simple impl for example
        private String fillUpSpace(String val, int num) {
            String charsetName = "MS932";
            int len;
            try {
                len = val.getBytes(charsetName).length;  // (5)
            } catch (UnsupportedEncodingException e) {
                // omitted exception handling
            }

            // (6)
            if (len > num) {
                throw new IncorrectFieldLengthException("The length of field is invalid. " + "[value:" + val + "][length:"
                        + len + "][expect length:" + num + "]");
            }

            if (num == len) {
                return val;
            }

            StringBuilder filledVal = new StringBuilder();
            for (int i = 0; i < (num - len); i++) {  // (7)
                filledVal.append(" ");
            }
            filledVal.append(val);

            return filledVal.toString();
        }
    }

表 17. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FieldExtractor`クラスを実装し、`extract`メソッドをオーバーライドする。 `FieldExtractor`の型引数には変換対象クラスを設定する。
(2) | 変換処理等を行ったデータを格納するためのObject型配列を定義する。
(3) | 引数で受けたitem(処理対象のBean)から値を取得し、適宜変換処理を行い、Object型の配列に格納する。
(4) | 全角文字が含まれるフィールドに対してフォーマット処理を行う。 フォーマット処理の詳細は(5)、(6)を参照。
(5) | 文字コードに対するバイト数を取得する。
(6) | 取得したバイト数が最大長を超えている場合は、例外をスローする。
(7) | 取得したバイト数をもとにパディング・トリム処理で整形する。 実装例では指定されたバイト数まで文字列の前に空白を付与している。
(8) | 処理結果を保持しているObject型の配列を返す。

### 単一文字列レコード

単一文字列レコードファイルを扱う場合の定義方法を説明する

#### 入力

下記の入力ファイルを読み込むための設定例を示す。

入力ファイル例

    Summary1:4,000,000,000
    Summary2:5,000,000,000
    Summary3:6,000,000,000

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemReader<String> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final FlatFileItemReader<String> reader = new FlatFileItemReader<>();
        final PassThroughLineMapper lineMapper = new PassThroughLineMapper(); // (4)
        reader.setResource(new FileSystemResource(inputFile)); // (1)
        reader.setEncoding("MS932"); // (2)
        reader.setStrict(true); // (3)
        reader.setLineMapper(lineMapper);
        return reader;
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="MS932"
          p:strict="true">
        <property name="lineMapper">  <!-- (4) -->
            <bean class="org.springframework.batch.item.file.mapping.PassThroughLineMapper"/>
        </property>
    </bean>

表 18. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(4) | lineMapper | `org.springframework.batch.item.file.mapping.PassThroughLineMapper`を設定する。 `PassThroughLineMapper`は渡されたレコードをそのまま文字列として返す`LineMapper`の実装クラスである。 |  | なし

#### 出力

下記の出力ファイルを書き出すための設定例を示す。

出力ファイル例

    Summary1:4,000,000,000
    Summary2:5,000,000,000
    Summary3:6,000,000,000

JavaConfig
Bean定義

    // Writer
    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        final PassThroughLineAggregator<SalesPlanDetail> lineAggregator = new PassThroughLineAggregator<>(); // (8)
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .lineSeparator("\n") // (3)
                .append(true) // (4)
                .shouldDeleteIfExists(false) // (5)
                .shouldDeleteIfEmpty(false) // (6)
                .transactional(false) // (7)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <!-- Writer -->
    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:lineSeparator="&#x0A;"
          p:appendAllowed="true"
          p:shouldDeleteIfExists="false"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true">
        <property name="lineAggregator">  <!-- (8) -->
            <bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
        </property>
    </bean>

表 19. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | lineSeparator | レコード区切り(改行コード)を設定する。 |  | システムプロパティの`line.separator`
(4) | appendAllowed | trueの場合、既存のファイルに追記をする。 trueの場合、shouldDeleteIfExistsの設定値は無効化されるため、注意が必要である。 |  | false
(5) | shouldDeleteIfExists | appendAllowedがtrueの場合は、この設定は無効化されるため、値を指定しないことを推奨する。 ファイル出力前に出力ファイルが存在していた場合の動作を設定する。 trueの場合、ファイル出力前に既にファイルが存在すれば削除する。 falseの場合、ファイル出力前に既にファイルが存在すれば例外をスローする。 |  | true
(6) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(7) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(8) | lineAggregator | `org.springframework.batch.item.file.transform.PassThroughLineAggregator`を設定する。 `PassThroughLineAggregator`はitem(処理対象のBean)をそのまま文字列へ変換(`item.toString()`を実行)する`LineAggregator`の実装クラスである。 |  | なし

### ヘッダとフッタ

ヘッダ・フッタがある場合の入出力方法を説明する。

ここでは行数指定にてヘッダ・フッタを読み飛ばす方法を説明する。
ヘッダ・フッタのレコード数が可変であり行数指定ができない場合は、マルチフォーマットの入力を参考に`PatternMatchingCompositeLineMapper`を使用すること。

#### 入力

##### ヘッダの読み飛ばし

ヘッダレコードを読み飛ばす方法には以下に示す2パターンがある。

- `FlatFileItemReader`の`linesToSkip`にファイルの先頭から読み飛ばす行数を設定

- OSコマンドによる前処理でヘッダレコードを取り除く

入力ファイル例

    sales_plan_detail_11
    branchId,year,month,customerId,amount
    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000

先頭から2行がヘッダレコードである。

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
linesToSkipによる読み飛ばし

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        final FlatFileItemReader<SalesPlanDetail> reader = new FlatFileItemReader<>();
        // omitted settings
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        reader.setLinesToSkip(2); // (1)
        return reader;
    }

XMLConfig
linesToSkipによる読み飛ばし

    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:linesToSkip="2">  <!-- (1) -->
        <property name="lineMapper">
            <!-- omitted settings -->
        </property>
    </bean>

表 20. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | linesToSkip | 読み飛ばすヘッダ行数を設定する。 |  | 0

OSコマンドによる読み飛ばし処理

    # Remove number of lines in header from the top of input file
    tail -n +`expr 2 + 1` input.txt > output.txt

tailコマンドを利用し、入力ファイルinput.txtの3行目以降を取得し、output.txtに出力している。 tailコマンドのオプション`-n +K`に指定する値はヘッダレコードの数+1となるため注意すること。

| ヘッダレコードとフッタレコードを読み飛ばすOSコマンド headコマンドとtailコマンドをうまく活用することでヘッダレコードとフッタレコードを行数指定をして読み飛ばすことが可能である。 ヘッダレコードの読み飛ばし方 tailコマンドをオプション`-n +K`を付与して実行することで、処理対象の`K`行目以降を取得する。 フッタレコードの読み飛ばし方 headコマンドをオプション`-n -K`を付与して実行することで、処理対象の末尾から`K`行目より前を取得する。 ヘッダレコードとフッタレコードをそれぞれ読み飛ばすシェルスクリプト例を下記に示す。 ヘッダ/フッタから指定行数を取り除くシェルスクリプトの例
    #!/bin/bash

    if [ $# -ne 4 ]; then
      echo "The number of arguments must be 4, given is $#." 1>&2
      exit 1
    fi

    # Input file.
    input=$1

    # Output file.
    output=$2

    # Number of lines in header.
    header=$3

    # Number of lines in footer.
    footer=$4

    # Remove number of lines in header from the top of input file
    # and number of lines in footer from the end,
    # and save to output file.
    tail -n +`expr ${header} + 1` ${input} | head -n -${footer} > ${output}
表 21. 引数
項番 | 説明
--- | ---
(1) | 入力ファイル
(2) | 出力ファイル
(3) | 読み飛ばすヘッダの行数
(4) | 読み飛ばすフッタの行数

##### ヘッダ情報の取り出し

ヘッダレコードを認識し、ヘッダレコードの情報を取り出す方法を示す。

ヘッダ情報の取り出しは以下の要領で実装する。

設定

  - `org.springframework.batch.item.file.LineCallbackHandler`の実装クラスにヘッダに対する処理を実装する

    - `LineCallbackHandler#handleLine()`内で取得したヘッダ情報を`stepExecutionContext`に格納する

  - `FlatFileItemReader`の`skippedLinesCallback`に`LineCallbackHandler`の実装クラスを設定する

  - `FlatFileItemReader`の`linesToSkip`にヘッダの行数を指定する

ファイル読み込みおよびヘッダ情報の取り出し

  - `linesToSkip`の設定によってスキップされるヘッダレコード1行ごとに`LineCallbackHandler#handleLine()`が呼び出される

    - ヘッダ情報が`stepExecutionContext`に格納される

取得したヘッダ情報を利用する

  - ヘッダ情報を`stepExecutionContext`から取得してデータ部の処理で利用する

ヘッダレコードの情報を取り出す際の実装例を示す。

JavaConfig
Bean定義

    @Bean
    public HoldHeaderLineCallbackHandler lineCallbackHandler() {
        return new HoldHeaderLineCallbackHandler();
    }

    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile,
            HoldHeaderLineCallbackHandler lineCallbackHandler) {
        final FlatFileItemReader<SalesPlanDetail> reader = new FlatFileItemReader<>();
        // omitted settings
        reader.setLinesToSkip(2); // (1)
        reader.setSkippedLinesCallback(lineCallbackHandler); // (2)
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       LoggingHeaderRecordItemProcessor processor,
                       LoggingItemReaderListener loggingItemReaderListener,
                       HoldHeaderLineCallbackHandler lineCallbackHandler) {
        return new StepBuilder("jobReadCsvSkipAndReferHeader.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .listener(loggingItemReaderListener)
                .listener(lineCallbackHandler) // (3)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobReadCsvSkipAndReferHeader(JobRepository jobRepository,
                                            Step step01,
                                            JobExecutionLoggingListener listener) {
        return new JobBuilder("jobReadCsvSkipAndReferHeader", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig
Bean定義

    <bean id="lineCallbackHandler"
          class="org.terasoluna.batch.functionaltest.ch05.fileaccess.module.HoldHeaderLineCallbackHandler"/>

    <!-- (1) (2) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:linesToSkip="2"
          p:skippedLinesCallback-ref="lineCallbackHandler"
          p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">
            <!-- omitted settings -->
        </property>
    </bean>

    <batch:job id="jobReadCsvSkipAndReferHeader" job-repository="jobRepository">
        <batch:step id="jobReadCsvSkipAndReferHeader.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="loggingHeaderRecordItemProcessor"
                             writer="writer" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="lineCallbackHandler"/>  <!-- (3) -->
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
    </batch:job>

表 22. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | linesToSkip | 読み飛ばすヘッダ行数を設定する。 |  | 0
(2) | skippedLinesCallback | `LineCallbackHandler`の実装クラスを設定する。 実装例は後述する。 |  | なし
(3) | listener | `StepExecutionListener`の実装クラスを設定する。 `FlatFileItemReader`の`skippedLinesCallback`に指定する`LineCallbackHandler`は自動で`Listener`として登録されないため設定が必須となる。 詳しい理由は後述する。 |  | なし

##### リスナー設定について

下記の2つの場合は自動で`Listener`として登録されないため、ジョブ定義時に`listenerメソッド/<batch:listeners>`にも定義を追加する必要がある。
(リスナの定義を追加しないと、`StepExecutionListener#beforeStep()`が実行されない)

- `FlatFileItemReader`の`skippedLinesCallback`に指定する`LineCallbackHandler`の`StepExecutionListener`

- `Tasklet`の実装クラスに実装する`StepExecutionListener`

JavaConfig

    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> reader,
                       ItemWriter<SalesPlanDetail> writer,
                       LoggingHeaderRecordItemProcessor processor,
                       LoggingItemReaderListener loggingItemReaderListener,
                       HoldHeaderLineCallbackHandler lineCallbackHandler) {
        return new StepBuilder("jobReadCsvSkipAndReferHeader.step01",
                jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(reader)
                .processor(processor)
                .listener(loggingItemReaderListener)
                // manantory
                .listener(lineCallbackHandler)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobReadCsvSkipAndReferHeader(JobRepository jobRepository,
                                            Step step01,
                                            JobExecutionLoggingListener listener) {
        return new JobBuilder("jobReadCsvSkipAndReferHeader", jobRepository)
                .start(step01)
                .listener(listener)
                .build();
    }

XMLConfig

    <batch:job id="jobReadCsvSkipAndReferHeader" job-repository="jobRepository">
        <batch:step id="jobReadCsvSkipAndReferHeader.step01">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="reader"
                             processor="loggingHeaderRecordItemProcessor"
                             writer="writer" commit-interval="10"/>
                <batch:listeners>
                    <batch:listener ref="loggingItemReaderListener"/>
                    <!-- mandatory -->
                    <batch:listener ref="lineCallbackHandler"/>
                </batch:listeners>
            </batch:tasklet>
        </batch:step>
    </batch:job>

`LineCallbackHandler`は以下の要領で実装する。

- `StepExecutionListener#beforeStep()`の実装

  - 下記のいずれかの方法で`StepExecutionListener#beforeStep()`を実装する

    - `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする

    - `beforeStep`メソッドを実装し、`@BeforeStep`アノテーションを付与する

  - `beforeStep`メソッドにて`StepExecution`を取得してクラスフィールドに保持する

- `LineCallbackHandler#handleLine()`の実装

  - `LineCallbackHandler`クラスを実装し、`handleLine`メソッドをオーバーライドする

    - `handleLine`メソッドはスキップする1行ごとに1回呼ばれる点に注意すること。

  - `StepExecution`から`stepExecutionContext`を取得し、`stepExecutionContext`にヘッダ情報を格納する。

LineCallbackHandlerの実装例

    @Component
    public class HoldHeaderLineCallbackHandler implements LineCallbackHandler {  // (1)
        private StepExecution stepExecution;  // (2)

        @BeforeStep  // (3)
        public void beforeStep(StepExecution stepExecution) {
            this.stepExecution = stepExecution;  // (4)
        }

        @Override  // (5)
        public void handleLine(String line) {
            this.stepExecution.getExecutionContext().putString("header", line);  // (6)
        }
    }

表 23. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `LineCallbackHandler`クラスを実装し、`handleLine`メソッドをオーバーライドする。
(2) | `StepExecution`を保持するためのフィールドを定義する。
(3) | `beforeStep`メソッドを実装し、`@BeforeStep`アノテーションを付与する。 シグネチャは`void beforeStep(StepExecution stepExecution)`とする。 `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする方法でもよい。
(4) | `StepExecution`を取得してクラスフィールドに保持する。
(5) | `LineCallbackHandler`クラスを実装し、`handleLine`メソッドをオーバーライドする。
(6) | `StepExecution`から`stepExecutionContext`を取得し、`header`というキーを指定して`stepExecutionContext`にヘッダ情報を格納する。 ここでは簡単のため、スキップする2行のうち、最後の1行だけを格納している。

ヘッダ情報を`stepExecutionContext`から取得してデータ部の処理で利用する例を示す。
`ItemProcessor`にてヘッダ情報を利用する場合を例にあげて説明する。
他のコンポーネントでヘッダ情報を利用する際も同じ要領で実現することができる。

ヘッダ情報を利用する処理は以下の要領で実装する。

- `LineCallbackHandler`の実装例と同様に`StepExecutionListener#beforeStep()`を実装する

- `beforeStep`メソッドにて`StepExecution`を取得してクラスフィールドに保持する

- `StepExecution`から`stepExecutionContext`およびヘッダ情報を取得して利用する

ヘッダ情報の利用例

    @Component
    public class LoggingHeaderRecordItemProcessor implements
            ItemProcessor<SalesPlanDetail, SalesPlanDetail> {
        private StepExecution stepExecution;  // (1)

        @BeforeStep  // (2)
        public void beforeStep(StepExecution stepExecution) {
            this.stepExecution = stepExecution;  // (3)
        }

        @Override
        public SalesPlanDetail process(SalesPlanDetail item) throws Exception {
            String headerData = this.stepExecution.getExecutionContext()
                    .getString("header");  // (4)
            // omitted business logic
            return item;
        }
    }

表 24. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `StepExecution`を保持するためのフィールドを定義する。
(2) | `beforeStep`メソッドを実装し、`@BeforeStep`アノテーションを付与する。 シグネチャは`void beforeStep(StepExecution stepExecution)`とする。 `StepExecutionListener`クラスを実装し、`beforeStep`メソッドをオーバーライドする方法でもよい。
(3) | `StepExecution`を取得してクラスフィールドに保持する。
(4) | `StepExecution`から`stepExecutionContext`を取得し、`header`というキーを指定して`stepExecutionContext`からヘッダ情報を取得する。

| Job/StepのExecutionContextの使用について ヘッダ(フッタ)情報の取出しでは、読み込んだヘッダ情報を`StepExecution`の`ExecutionContext`に格納しておき、使用する際に`ExecutionContext`から取り出す方式をとる。 下記の例では1つのステップ内でヘッダ情報の取得および利用を行うため`StepExecution`の`ExecutionContext`へヘッダ情報を格納している。 ヘッダ情報の取得および利用にてステップが分かれる場合は`JobExecution`の`ExecutionContext`を利用すること。 Job/Stepの`ExecutionContext`に関する詳細は、Spring Batchのアーキテクチャを参照。

##### フッタの読み飛ばし

Spring BatchおよびTERASOLUNA Batch 5.xでは、フッタレコードの読み飛ばし機能は提供していないため、OSコマンドで対応する。

入力ファイル例

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000
    number of items,3
    total of amounts,6000000000

末尾から2行がフッタレコードである。

上記のファイルを読む込むための設定は以下のとおり。

OSコマンドによる読み飛ばし処理

    $ # Remove number of lines in footer from the end of input file
    $ head -n -2 input.txt > output.txt

headコマンドを利用し、入力ファイルinput.txtの末尾から2行目より前を取得し、output.txtに出力している。

##### フッタ情報の取り出し

Spring BatchおよびTERASOLUNA Batch 5.xでは、フッタレコードの読み飛ばし機能、フッタ情報の取得機能は提供していない。

そのため、処理を下記ようにOSコマンドによる前処理と2つのステップに分割することで対応する。

- OSコマンドによってフッタレコードを分割する

- 1つめのステップにてフッタレコードを読み込み、フッタ情報を`ExecutionContext`に格納する

- 2つめのステップにて`ExecutionContext`からフッタ情報を取得し、利用する

フッタ情報を取り出しは以下の要領で実装する。

OSコマンドによるフッタレコードの分割

  - OSコマンドを利用して入力ファイルをフッタ部とフッタ部以外に分割する

1つめのステップでフッタレコードを読み込み、フッタ情報を取得する

  - フッタレコードを読み込み`jobExecutionContext`に格納する

    - フッタ情報の格納と利用にてステップが異なるため、`jobExecutionContext`に格納する。

    - `jobExecutionContext`を利用する方法は、JobとStepのスコープに関する違い以外は、ヘッダ情報の取り出しにて説明した`stepExecutionContext`と同様である。

2つめのステップにて取得したフッタ情報を利用する

  - フッタ情報を`jobExecutionContext`から取得してデータ部の処理で利用する

以下に示すファイルのフッタ情報を取り出して利用する場合を例にあげて説明する。

入力ファイル例

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000
    number of items,3
    total of amounts,6000000000

末尾から2行がフッタレコードである。

OSコマンドによるフッタレコードの分割

上記のファイルをOSコマンドを利用してフッタ部とフッタ部以外に分割する設定は以下のとおり。

OSコマンドによる読み飛ばし処理

    $ # Extract non-footer record from input file and save to output file.
    $ head -n -2 input.txt > input_data.txt

    $ # Extract footer record from input file and save to output file.
    $ tail -n 2 input.txt > input_footer.txt

headコマンドを利用し、入力ファイルinput.txtのフッタ部以外をinput_data.txtへ、フッタ部をinput_footer.txtに出力している。

出力ファイル例は以下のとおり。

出力ファイル例(input_data.txt)

    000001,2016,1,0000000001,1000000000
    000002,2017,2,0000000002,2000000000
    000003,2018,3,0000000003,3000000000

出力ファイル例(input_footer.txt)

    number of items,3
    total of amounts,6000000000

フッタ情報の取得、利用

OSコマンドにて分割したフッタレコードからフッタ情報を取得、利用する方法を説明する。

フッタレコードを読み込むステップを前処理として主処理とステップを分割している。
ステップの分割に関する詳細は、フロー制御を参照。

下記の例ではフッタ情報を取得し、`jobExecutionContext`へフッタ情報を格納するまでの例を示す。
`jobExecutionContext`からフッタ情報を取得し利用する方法はヘッダ情報の取り出しと同じ要領で実現可能である。

データレコードの情報を保持するクラス

    public class SalesPlanDetail {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

フッタレコードの情報を保持するクラス

    public class SalesPlanDetailFooter implements Serializable {

        // omitted serialVersionUID

        private String name;
        private String value;

        // omitted getter/setter
    }

下記の要領でBean定義を行う。

- フッタレコードを読み込む`ItemReader`を定義する

- データレコードを読み込む`ItemReader`を定義する

- フッタレコードを取得するビジネスロジックを定義する

  - 下記の例では`Tasklet`の実装クラスで実現している

- ジョブを定義する

  - フッタ情報を取得する前処理ステップとデータレコードを読み込み主処理を行うステップを定義する

JavaConfig
Bean定義

    // (1)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetailFooter> footerReader(
            @Value("#{jobParameters['footerInputFile']}") File inputFile) {
        // omitted other settings
        final DefaultLineMapper<SalesPlanDetailFooter> lineMapper = new DefaultLineMapper<>();
        // omitted other settings
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

    // (2)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPlanDetail> dataReader(
            @Value("#{jobParameters['dataInputFile']}") File inputFile) {
        // omitted other settings
        final DefaultLineMapper<SalesPlanDetail> lineMapper = new DefaultLineMapper<>();
        // omitted other settings
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteFooterFlatFileFooterCallback writeFooterFlatFileFooterCallback) {
        // omitted other settings
    }

    // Tasklet for reading footer records
    @Bean
    @JobScope
    public ReadFooterTasklet readFooterTasklet() {
        return new ReadFooterTasklet();
    }

    // (3)
    @Bean
    public Step step01(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ReadFooterTasklet readFooterTasklet) {
        return new StepBuilder("jobReadAndWriteCsvWithFooter.step01", jobRepository)
                .tasklet(readFooterTasklet, transactionManager)
                .build();
    }

    // (4)
    @Bean
    public Step step02(JobRepository jobRepository,
                       @Qualifier("jobTransactionManager") PlatformTransactionManager transactionManager,
                       ItemReader<SalesPlanDetail> dataReader,
                       ItemWriter<SalesPlanDetail> writer) {
        return new StepBuilder("jobReadAndWriteCsvWithFooter.step02", jobRepository)
                .<SalesPlanDetail, SalesPlanDetail> chunk(10, transactionManager)
                .reader(dataReader)
                .writer(writer)
                .build();
    }

    @Bean
    public Job jobReadAndWriteCsvWithFooter(JobRepository jobRepository,
                                            Step step01,
                                            Step step02,
                                            JobExecutionLoggingListener jobExecutionLoggingListener,
                                            ReadFooterTasklet readFooterTasklet,
                                            WriteFooterFlatFileFooterCallback writeFooterFlatFileFooterCallback) {
        return new JobBuilder("jobReadAndWriteCsvWithFooter", jobRepository)
                .start(step01)
                .next(step02)
                .listener(jobExecutionLoggingListener)
                .listener(readFooterTasklet) // (5)
                .listener(writeFooterFlatFileFooterCallback)
                .build();
    }

XMLConfig
Bean定義

    <!-- ItemReader for reading footer records -->
    <!-- (1) -->
    <bean id="footerReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['footerInputFile']}">
        <property name="lineMapper">
            <!-- omitted other settings -->
        </property>
    </bean>

    <!-- ItemReader for reading data records -->
    <!-- (2) -->
    <bean id="dataReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['dataInputFile']}">
        <property name="lineMapper">
            <!-- omitted other settings -->
        </property>
    </bean>

    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
      <!-- omitted settings -->
    </bean>

    <!-- Tasklet for reading footer records -->
    <bean id="readFooterTasklet"
          class="org.terasoluna.batch.functionaltest.ch05.fileaccess.module.ReadFooterTasklet"/>

    <batch:job id="jobReadAndWriteCsvWithFooter" job-repository="jobRepository">
        <!-- (3) -->
        <batch:step id="jobReadAndWriteCsvWithFooter.step01"
                next="jobReadAndWriteCsvWithFooter.step02">
            <batch:tasklet ref="readFooterTasklet"
                           transaction-manager="jobTransactionManager"/>
        </batch:step>
        <!-- (4) -->
        <batch:step id="jobReadAndWriteCsvWithFooter.step02">
            <batch:tasklet transaction-manager="jobTransactionManager">
                <batch:chunk reader="dataReader"
                             writer="writer" commit-interval="10"/>
            </batch:tasklet>
        </batch:step>
        <batch:listeners>
            <batch:listener ref="readFooterTasklet"/> <!-- (5) -->
        </batch:listeners>
    </batch:job>

表 25. 設定内容の項目一覧
項番 | 項目 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | footerReader | フッタレコードを保持するファイルを読み込むための`ItemReader`を定義する。 フッタ情報を取得するステップで実行される`readFooterTasklet`にてインジェクトして使用する。 |  |
(2) | dataReader | データレコードを保持するファイルを読み込むための`ItemReader`を定義する。 |  |
(3) | 前処理ステップ | フッタ情報を取得するステップを定義する。 処理は`readFooterTasklet`に実装している。実装例は後述する。 |  |
(4) | 主処理ステップ | データ情報を取得するとともにフッタ情報を利用するステップを定義する。 `reader`には`dataReader`を使用する。 例ではフッタ情報を`jobExecutionContext`から取得し利用する処理(`ItemProcessor`等)は実装していない。 フッタ情報を取得し利用する方法はヘッダ情報の取り出しと同じ要領で実現可能である。 |  |
(5) | listeners | `readFooterTasklet`を設定する。 この設定を行わないと`readFooterTasklet`内に実装する`JobExecutionListener#beforeJob()`が実行されない。 詳しい理由は、ヘッダ情報の取り出しを参照。 |  | なし

フッタレコードを保持するファイルを読み込み、`jobExecutionContext`に格納する処理を行う処理の例を示す。

`Tasklet`の実装クラスとして実現する際の要領は以下のとおり。

- Bean定義した`footerReader`を`@Inject`アノテーションと`@Named`アノテーションを使用し名前指定でインジェクトする。

- 読み込んだフッタ情報を`jobExecutionContext`に格納する

  - 実現方法はヘッダ情報の取り出しと同様である

フッタ情報の取得

    public class ReadFooterTasklet implements Tasklet {
        // (1)
        @Inject
        @Named("footerReader")
        ItemStreamReader<SalesPlanDetailFooter> itemReader;

        private JobExecution jobExecution;

        @BeforeJob
        public void beforeJob(JobExecution jobExecution) {
            this.jobExecution = jobExecution;
        }

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {
            ArrayList<SalesPlanDetailFooter> footers = new ArrayList<>();

            // (2)
            itemReader.open(chunkContext.getStepContext().getStepExecution()
                    .getExecutionContext());

            SalesPlanDetailFooter footer;
            while ((footer = itemReader.read()) != null) {
                footers.add(footer);
            }

            // (3)
            jobExecution.getExecutionContext().put("footers", footers);

            return RepeatStatus.FINISHED;
        }
    }

表 26. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | Bean定義した`footerReader`を`@Inject`アノテーションと`@Named`アノテーションを使用し名前指定でインジェクトする。
(2) | `footerReader`を使用してフッタレコードを保持したファイルを読み込みフッタ情報を取得する。 `Tasklet`の実装クラス内でBean定義した`ItemReader`を使用する方法はタスクレット指向ジョブの作成を参照。
(3) | `JobExecution`から`jobExecutionContext`を取得し、`footers`というキーを指定して`jobExecutionContext`へフッタ情報を格納する。

#### 出力

##### ヘッダ情報の出力

フラットファイルでヘッダ情報を出力する際は以下の要領で実装する。

- `org.springframework.batch.item.file.FlatFileHeaderCallback`の実装を行う

- 実装した`FlatFileHeaderCallback`を`FlatFileItemWriter`の`headerCallback`に設定する

  - `headerCallback`を設定すると`FlatFileItemWriter`の出力処理で、最初に`FlatFileHeaderCallback#writeHeader()`が実行される

`FlatFileHeaderCallback`は以下の要領で実装する。

- `FlatFileHeaderCallback`クラスを実装し、`writeHeader`メソッドをオーバーライドする

- 引数で受ける`Writer`を用いてヘッダ情報を出力する。

下記に`FlatFileHeaderCallback`クラスの実装例を示す。

FlatFileHeaderCallbackの実装例

    @Component
    // (1)
    public class WriteHeaderFlatFileFooterCallback implements FlatFileHeaderCallback {
        @Override
        public void writeHeader(Writer writer) throws IOException {
            // (2)
            writer.write("omitted");
        }
    }

表 27. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FlatFileHeaderCallback`クラスを実装し、`writeHeader`メソッドをオーバーライドする。
(2) | 引数で受ける`Writer`を用いてヘッダ情報を出力する。 `FlatFileHeaderCallback#writeHeader()`の実行直後に`FlatFileItemWriter`が出力する処理を実行する。 そのため、ヘッダ情報末尾の改行は出力不要である。 出力される改行は、Bean定義時に`FlatFileItemWriter`に指定したものである。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteHeaderFlatFileFooterCallback writeHeaderFlatFileFooterCallback) {
        // omitted settings
        return new FlatFileItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .headerCallback(writeHeaderFlatFileFooterCallback) // (1)
                .lineSeparator("\n") // (2)
                .resource(new FileSystemResource(outputFile))
                .transactional(false)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:headerCallback-ref="writeHeaderFlatFileFooterCallback"
          p:lineSeparator="&#x0A;"
          p:resource="file:#{jobParameters['outputFile']}">
        <property name="lineAggregator">
            <!-- omitted settings -->
        </property>
    </bean>

表 28. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | headerCallback | `FlatFileHeaderCallback`の実装クラスを設定する。 |  |
(2) | lineSeparator | レコード区切り(改行コード)を設定する。 |  | システムプロパティの`line.separator`

| FlatFileHeaderCallback実装時にヘッダ情報末尾の改行は出力不要 `FlatFileItemWriter`内で`FlatFileHeaderCallback#writeHeader()`の実行直後にBean定義時に指定した改行を出力する処理が実行されるため、ヘッダ情報末尾の改行は出力不要である。

##### フッタ情報の出力

フラットファイルでフッタ情報を出力する際は以下の要領で実装する。

- `org.springframework.batch.item.file.FlatFileFooterCallback`の実装を行う

- 実装した`FlatFileFooterCallback`を`FlatFileItemWriter`の`footerCallback`に設定する

  - `footerCallback`を設定すると`FlatFileItemWriter`の出力処理で、最後に`FlatFileFooterCallback#writeFooter()`が実行される

フラットファイルでフッタ情報を出力する方法について説明する。

`FlatFileFooterCallback`は以下の要領で実装する。

- 引数で受ける`Writer`を用いてフッタ情報を出力する。

- `FlatFileFooterCallback`クラスを実装し、`writeFooter`メソッドをオーバーライドする

下記にJobの`ExecutionContext`からフッタ情報を取得し、ファイルへ出力する`FlatFileFooterCallback`クラスの実装例を示す。

フッタレコードの情報を保持するクラス

    public class SalesPlanDetailFooter implements Serializable {

        // omitted serialVersionUID

        private String name;
        private String value;

        // omitted getter/setter
    }

FlatFileFooterCallbackの実装例

    @Component
    public class WriteFooterFlatFileFooterCallback implements FlatFileFooterCallback {  // (1)
        private JobExecution jobExecution;

        @BeforeJob
        public void beforeJob(JobExecution jobExecution) {
            this.jobExecution = jobExecution;
        }

        @Override
        public void writeFooter(Writer writer) throws IOException {
            @SuppressWarnings("unchecked")
            ArrayList<SalesPlanDetailFooter> footers = (ArrayList<SalesPlanDetailFooter>) this.jobExecution.getExecutionContext().get("footers");  // (2)

            BufferedWriter bufferedWriter = new BufferedWriter(writer);  // (3)
            // (4)
            for (SalesPlanDetailFooter footer : footers) {
                bufferedWriter.write(footer.getName() +" is " + footer.getValue());
                bufferedWriter.newLine();
                bufferedWriter.flush();
            }
        }
    }

表 29. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FlatFileFooterCallback`クラスを実装し、`writeFooter`メソッドをオーバーライドする。
(2) | Jobの`ExecutionContext`から`footers`というkeyを指定してフッタ情報を取得する。 例ではArrayListで複数のフッタ情報を取得している。
(3) | 例では改行の出力に`BufferedWriter.newLine()`を使用するため、引数で受ける`Writer`を引数として`BufferedWriter`を生成する。
(4) | 引数で受ける`Writer`を用いてフッタ情報を出力する。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public FlatFileItemWriter<SalesPlanDetail> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteFooterFlatFileFooterCallback writeFooterFlatFileFooterCallback) {
        // omitted settings
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .footerCallback(writeFooterFlatFileFooterCallback) // (1)
                .transactional(false)
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義

    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:footerCallback-ref="writeFooterFlatFileFooterCallback">  <!-- (1) -->
        <property name="lineAggregator">
            <!-- omitted settings -->
        </property>
    </bean>

表 30. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | footerCallback | `FlatFileFooterCallback`の実装クラスを設定する。 |  |

### 複数ファイル

複数ファイルを扱う場合の定義方法を説明する。

#### 入力

同一レコード形式の複数ファイルを読み込む場合は、`org.springframework.batch.item.file.MultiResourceItemReader`を利用する。
`MultiResourceItemReader`は指定された`ItemReader`を使用し正規表現で指定された複数のファイルを読み込むことができる。

`MultiResourceItemReader`は以下の要領で定義する。

- `MultiResourceItemReader`のBeanを定義する

  - `resources`プロパティに読み込み対象のファイルを指定する

    - 正規表現で複数ファイルを指定する

  - `delegate`プロパティにファイル読み込みに利用する`ItemReader`を指定する

下記に示す複数のファイルを読み込む`MultiResourceItemReader`の定義例は以下のとおりである。

読み込み対象ファイル(ファイル名)

    sales_plan_detail_01.csv
    sales_plan_detail_02.csv
    sales_plan_detail_03.csv

JavaConfig
Bean定義

    @Bean
    @StepScope
    public MultiResourceItemReader<SalesPlanDetail> multiResourceReader(
            @Value("file:input/sales_plan_detail_*.csv") Resource[] resources,
            FlatFileItemReader<SalesPlanDetail> reader) {
        return new MultiResourceItemReaderBuilder<SalesPlanDetail>()
        .name(ClassUtils.getShortName(MultiResourceItemReader.class))
        .resources(resources) // (1)
        .delegate(reader) // (2)
        .build();
    }

    // (3)
    @Bean
    public FlatFileItemReader<SalesPlanDetail> reader() {
        final FlatFileItemReader<SalesPlanDetail> reader = new FlatFileItemReader<>();
        // omitted settings
        reader.setLineMapper(lineMapper);
        return reader;
    }

XMLConfig
Bean定義

    <!-- (1) (2) -->
    <bean id="multiResourceReader"
          class="org.springframework.batch.item.file.MultiResourceItemReader"
          scope="step"
          p:resources="file:input/sales_plan_detail_*.csv"
          p:delegate-ref="reader"/>

    <!-- (3) -->
    <bean id="reader"
          class="org.springframework.batch.item.file.FlatFileItemReader">
        <property name="lineMapper">
          <!-- omitted settings -->
        </property>
    </bean>

表 31. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 正規表現で複数の入力ファイルを設定する。 |  | なし
(2) | delegate | 実際にファイルを読み込み処理する`ItemReader`を設定する。 |  | なし
(3) | 実際にファイルを読み込み処理する`ItemReader` | `resource`プロパティは、`MultiResourceItemReader`から自動的に設定されるため、Bean定義に設定は不要である。 |  |

| MultiResourceItemReaderが使用するItemReaderにresourceの指定は不要である `MultiResourceItemReader`から委譲される`ItemReader`の`resource`は、`MultiResourceItemReader`から自動的に設定されるため、Bean定義に設定は不要である。

#### 出力

複数ファイルを扱う場合の定義方法を説明する。

一定の件数ごとに異なるファイルへ出力する場合は、`org.springframework.batch.item.file.MultiResourceItemWriter`を利用する。

`MultiResourceItemWriter`は指定された`ItemWriter`を使用して指定した件数ごとに複数ファイルへ出力することができる。
出力対象のファイル名は重複しないように一意にする必要があるが、そのための仕組みとして`ResourceSuffixCreator`が提供されている。
`ResourceSuffixCreator`はファイル名が一意となるようなサフィックスを生成するクラスである。

たとえば、出力対象ファイルを`outputDir/customer_list_01.csv`(`01`の部分は連番)というファイル名にしたい場合は下記のように設定する。

- `MultiResourceItemWriter`に`outputDir/customer_list_`と設定する

- サフィックス`01.csv`(`01`の部分は連番)を生成する処理を`ResourceSuffixCreator`に実装する

  - 連番は`MultiResourceItemWriter`から自動で増分されて渡される値を使用することができる

- 実施に使用される`ItemWriter`には`outputDir/customer_list_01.csv`が設定される

`MultiResourceItemWriter`は以下の要領で定義する。`ResourceSuffixCreator`の実装方法は後述する。

- `ResourceSuffixCreator`の実装クラスを定義する

- `MultiResourceItemWriter`のBeanを定義する

  - `resources`プロパティに出力対象のファイルを指定する

    - `ResourceSuffixCreator`の実装クラスで付与するサフィックスまでを設定

  - `resourceSuffixCreator`プロパティにサフィックスを生成する`ResourceSuffixCreator`の実装クラスを指定する

  - `delegate`プロパティにファイル読み込みに利用する`ItemWriter`を指定する

  - `itemCountLimitPerResource`プロパティに1ファイルあたりの出力件数を指定する

JavaConfig
Bean定義

    @Bean
    @StepScope
    public MultiResourceItemWriter<Customer> multiResourceItemWriter(
            @Value("#{jobParameters['outputDir']}") File outputDir,
            CustomerListResourceSuffixCreator customerListResourceSuffixCreator,
            FlatFileItemWriter<Customer> writer) {
        return new MultiResourceItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(MultiResourceItemWriter.class))
                .resource(new FileSystemResource(outputDir)) // (1)
                .resourceSuffixCreator(customerListResourceSuffixCreator) // (2)
                .delegate(writer) // (3)
                .itemCountLimitPerResource(4) // (4)
                .build();
    }

    // (5)
    @Bean
    public FlatFileItemWriter<Customer> writer() {
        // omitted settings
        final DelimitedLineAggregator<Customer> lineAggregator = new DelimitedLineAggregator<>();
        lineAggregator.setFieldExtractor(fieldExtractor);
        return new FlatFileItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .transactional(false)
                .lineAggregator(lineAggregator)
                .build();
    }

    // (6)
    @Bean
    public CustomerListResourceSuffixCreator customerListResourceSuffixCreator() {
        return new CustomerListResourceSuffixCreator();
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) (4) -->
    <bean id="multiResourceItemWriter"
          class="org.springframework.batch.item.file.MultiResourceItemWriter"
          scope="step"
          p:resource="file:#{jobParameters['outputDir']}"
          p:resourceSuffixCreator-ref="customerListResourceSuffixCreator"
          p:delegate-ref="writer"
          p:itemCountLimitPerResource="4"/>

    <!-- (5) -->
    <bean id="writer"
          class="org.springframework.batch.item.file.FlatFileItemWriter">
        <property name="lineAggregator">
            <!-- omitted settings -->
        </property>
    </bean>

    <bean id="customerListResourceSuffixCreator"
          class="org.terasoluna.batch.functionaltest.ch05.fileaccess.module.CustomerListResourceSuffixCreator"/>  <!-- (6) -->

表 32. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力対象ファイルのサフィックスを付与する前の状態を設定する。 `ItemWriter`には、`MultiResourceItemWriter`が自動でサフィックスを付与したものが設定される。 |  | なし
(2) | resourceSuffixCreator | `ResourceSuffixCreator`の実装クラスを設定する。 デフォルト値は`"." + index`というサフィックスを生成する`org.springframework.batch.item.file.SimpleResourceSuffixCreator`である。 |  | `SimpleResourceSuffixCreator`
(3) | delegate | 実際にファイルを書き込み処理する`ItemWriter`を設定する。 |  | なし
(4) | itemCountLimitPerResource | 1ファイルあたりの出力件数を設定する。 |  | `Integer.MAX_VALUE`
(5) | 実際にファイルを書き込み処理する`ItemWriter` | `resource`プロパティは、`MultiResourceItemWriter`から自動的に設定されるため、Bean定義に設定は不要である。 |  |
(6) | `ResourceSuffixCreator`の実装クラス | サフィックスを生成する`ResourceSuffixCreator`の実装クラスを定義する。 実装方法は後述する。 |  |

| MultiResourceItemWriterが使用するItemWriterにresourceの指定は不要である `MultiResourceItemWriter`から委譲される`ItemWriter`の`resource`は、`MultiResourceItemWriter`から自動的に設定されるため、Bean定義に設定は不要である。

`ResourceSuffixCreator`は以下の要領で実装する。

- `ResourceSuffixCreator`クラスを実装し、`getSuffix`メソッドをオーバーライドする

- 引数で受ける`index`を用いてサフィックスを生成して返り値として返す

  - `index`は初期値`1`で始まり出力対象ファイルごとにインクリメントされる`int`型の値である

ResourceSuffixCreatorの実装例

    // (1)
    public class CustomerListResourceSuffixCreator implements ResourceSuffixCreator {
        @Override
        public String getSuffix(int index) {
            return String.format("%02d", index) + ".csv";  // (2)
        }
    }

表 33. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ResourceSuffixCreator`クラスを実装し、`getSuffix`メソッドをオーバーライドする。
(2) | 引数で受ける`index`を用いてサフィックスを生成して返り値として返す。 `index`は初期値`1`で始まり出力対象ファイルごとにインクリメントされる`int`型の値である。

### コントロールブレイク

コントロールブレイクの実現方法について説明する。

コントロールブレイクとは

  コントロールブレイク処理(またはキーブレイク処理)とは、ソート済みのレコードを順次読み込み、 レコード内にある特定の項目(キー項目)が同じレコードを1つのグループとして処理する手法のことを指す。
  主にデータを集計するときに用いられ、 キー項目が同じ値の間は集計を続け、キー項目が異なる値になる際に集計値を出力する、 というアルゴリズムになる。

コントロールブレイク処理をするためには、グループの変わり目を判定するために、レコードを先読みする必要がある。 `org.springframework.batch.item.support.SingleItemPeekableItemReader`を使うことで先読みを実現できる。
また、コントロールブレイクはタスクレットモデルでのみ処理可能とする。 これは、チャンクが前提とする「1行で定義するデータ構造をN行処理する」や「一定件数ごとのトランザクション境界」といった点が、 コントロールブレイクの「グループの変わり目で処理をする」という点と合わないためである。

コントロールブレイク処理の実行タイミングと比較条件を以下に示す。

- 対象レコード処理前にコントロールブレイク実施

  - 前回読み取ったレコードを保持し、前回レコードと現在読み込んだレコードとの比較

- 対象レコード処理後にコントロールブレイク実施

  - `SingleItemPeekableItemReader`により次のレコードを先読みし、次レコードと現在読み込んだレコードとの比較

下記に入力データから処理結果を出力するコントロールブレイクの実装例を示す。

入力データ

    01,2016,10,1000
    01,2016,11,1500
    01,2016,12,1300
    02,2016,12,900
    02,2016,12,1200

処理結果

    Header Branch Id : 01,,,
    01,2016,10,1000
    01,2016,11,1500
    01,2016,12,1300
    Summary Branch Id : 01,,,3800
    Header Branch Id : 02,,,
    02,2016,12,900
    02,2016,12,1200
    Summary Branch Id : 02,,,2100

コントロールブレイクの実装例

    @Component
    public class ControlBreakTasklet implements Tasklet {

        @Inject
        SingleItemPeekableItemReader<SalesPerformanceDetail> reader; // (1)

        @Inject
        ItemStreamWriter<SalesPerformanceDetail> writer;

        @Override
        public RepeatStatus execute(StepContribution contribution,
                ChunkContext chunkContext) throws Exception {

            // omitted.

            SalesPerformanceDetail previousData = null;   // (2)
            BigDecimal summary = new BigDecimal(0);  //(3)

            List<SalesPerformanceDetail> items = new ArrayList<>();   // (4)

            try {
                reader.open(executionContext);
                writer.open(executionContext);

                while (reader.peek() != null) {   // (5)
                    SalesPerformanceDetail data = reader.read(); // (6)

                    // (7)
                    if (isBreakByBranchId(previousData, data)) {
                        SalesPerformanceDetail beforeBreakData =
                                new SalesPerformanceDetail();
                        beforeBreakData.setBranchId("Header Branch Id : "
                                  + currentData.getBranchId());
                        items.add(beforeBreakData);
                    }

                    // omitted.
                    items.add(data);  // (8)

                    SalesPerformanceDetail nextData = reader.peek();  // (9)
                    summary = summary.add(data.getAmount());

                    // (10)
                    SalesPerformanceDetail afterBreakData = null;
                    if (isBreakByBranchId(nextData, data)) {
                        afterBreakData = new SalesPerformanceDetail();
                        afterBreakData.setBranchId("Summary Branch Id : "
                                + currentData.getBranchId());
                        afterBreakData.setAmount(summary);
                        items.add(afterBreakData);
                        summary = new BigDecimal(0);
                        writer.write(new Chunk(items));  // (11)
                        items.clear();
                    }
                    previousData = data;  // (12)
                }
            } finally {
                try {
                    reader.close();
                } catch (ItemStreamException e) {
                }
                try {
                    writer.close();
                } catch (ItemStreamException e) {
                }
            }
            return RepeatStatus.FINISHED;
        }
        // (13)
        private boolean isBreakByBranchId(SalesPerformanceDetail o1,
                SalesPerformanceDetail o2) {
            return (o1 == null || !o1.getBranchId().equals(o2.getBranchId()));
        }
    }

表 34. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `SingleItemPeekableItemReader`をInjectする。
(2) | 前回読み取ったレコードを保持する変数を定義する。
(3) | グループごとの集計値を格納する変数を定義する。
(4) | コントロールブレイクの処理結果を含めたグループ単位のレコードを格納する変数を定義する。
(5) | 入力データが無くなるまで処理を繰り返す。
(6) | 処理対象のレコードを読み込む。
(7) | 対象レコード処理前にコントロールブレイクを実施する。 ここではグループの先頭であれば見出しを設定して、(4)で定義した変数に格納する。
(8) | 対象レコードへの処理結果を(4)で定義した変数に格納する。
(9) | 次のレコードを先読みする。
(10) | 対象レコード処理後にコントロールブレイクを実施する。 ここではグループの末尾であれば集計データをトレーラに設定して、(4)で定義した変数に格納する。
(11) | グループ単位で処理結果を出力する。
(12) | 処理レコードを(2)で定義した変数に格納する。
(13) | キー項目が切り替わったか判定する。

JavaConfig
Bean定義

    // (1)
    @Bean
    public SingleItemPeekableItemReader<SalesPerformanceDetail> reader(
            FlatFileItemReader<SalesPerformanceDetail> delegateReader) {
        return new SingleItemPeekableItemReaderBuilder<SalesPerformanceDetail>()
                .delegate(delegateReader) // (2)
                .build();
    }

    // (3)
    @Bean
    @StepScope
    public FlatFileItemReader<SalesPerformanceDetail> delegateReader(
            @Value("#{jobParameters['inputFile']}") File inputFile) {
        FlatFileItemReader<SalesPerformanceDetail> delegateReader = new FlatFileItemReader<>();
        DefaultLineMapper<SalesPerformanceDetail> lineMapper = new DefaultLineMapper<>();
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setNames("branchId", "year", "month", "customerId", "amount");
        BeanWrapperFieldSetMapper<SalesPerformanceDetail> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setTargetType(SalesPerformanceDetail.class);
        lineMapper.setLineTokenizer(lineTokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        delegateReader.setLineMapper(lineMapper);
        delegateReader.setResource(new FileSystemResource(inputFile));
        return delegateReader;
    }

XMLConfig
Bean定義

    <!-- (1) -->
    <bean id="reader"
          class="org.springframework.batch.item.support.SingleItemPeekableItemReader"
          p:delegate-ref="delegateReader" />  <!-- (2) -->

    <!-- (3) -->
    <bean id="delegateReader"
          class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">
            <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
                <property name="lineTokenizer">
                    <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
                          p:names="branchId,year,month,customerId,amount"/>
                </property>
                <property name="fieldSetMapper">
                    <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
                          p:targetType="org.terasoluna.batch.functionaltest.app.model.performance.SalesPerformanceDetail"/>
                </property>
            </bean>
        </property>
    </bean>

表 35. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `SingleItemPeekableItemReader`をBean定義する。TaskletへのInject対象。
(2) | `delegate-ref`属性に実際にファイルを読み込むItemReaderのBeanを指定する。
(3) | 実際にファイルを読み込むItemReaderのBeanを定義する。

## How to extend

ここでは、以下のケースについて説明する。

- FieldSetMapperの実装

- XMLファイルの入出力

- マルチフォーマットの入出力

### FieldSetMapperの実装

`FieldSetMapper`を自作で実装する方法について説明する。

`FieldSetMapper`の実装クラスは下記の要領で実装する。

- `FieldSetMapper`クラスを実装し、`mapFieldSet`メソッドをオーバーライドする

- 引数で受けた`FieldSet`から値を取得し、適宜変換処理を行い、変換対象のBeanに格納し返り値として返す

  - `FieldSet`クラスはJDBCにある`ResultSet`クラスのようにインデックスまたは名前と関連付けてデータを保持するクラスである

  - `FieldSet`クラスは`LineTokenizer`によって分割されたレコードの各フィールドの値を保持する

  - インデックスまたは名前を指定して値を格納および取得することができる

下記のような西暦フォーマットのLocalDate型やカンマを含むBigDecimal型の変換を行うファイルを読み込む場合の実装例を示す。

入力ファイル例

    "000001","2016年1月1日","000000001","1,000,000,000"
    "000002","2017年2月2日","000000002","2,000,000,000"
    "000003","2018年3月3日","000000003","3,000,000,000"

表 36. 入力ファイル仕様
項番 | フィールド名 | データ型 | 備考
--- | --- | --- | ---
(1) | branchId | String |
(2) | 日付 | LocalDate | 西暦フォーマット
(3) | customerId | String |
(4) | amount | BigDecimal | カンマを含む

変換対象クラス

    public class UseDateSalesPlanDetail {

        private String branchId;
        private LocalDate date;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

FieldSetMapperの実装例

    @Component
    public class UseDateSalesPlanDetailFieldSetMapper implements FieldSetMapper<UseDateSalesPlanDetail> {  // (1)
        /**
         * {@inheritDoc}
         *
         * @param fieldSet {@inheritDoc}
         * @return Sales performance detail.
         * @throws BindException {@inheritDoc}
         */
        @Override
        public UseDateSalesPlanDetail mapFieldSet(FieldSet fieldSet) throws BindException {
            UseDateSalesPlanDetail item = new UseDateSalesPlanDetail();  // (2)

            item.setBranchId(fieldSet.readString("branchId"));  // (3)

            // (4)
            DateTimeFormatter japaneseFormat = DateTimeFormatter.ofPattern("yyyy年M月d日");
            try {
                item.setDate(LocalDate.parse(fieldSet.readString("date"), japaneseFormat));
            } catch (DateTimeParseException e) {
                // omitted exception handling
            }

            // (5)
            item.setCustomerId(fieldSet.readString("customerId"));

            // (6)
            DecimalFormat decimalFormat = new DecimalFormat();
            decimalFormat.setParseBigDecimal(true);
            try {
                item.setAmount((BigDecimal) decimalFormat.parse(fieldSet.readString("amount")));
            } catch (ParseException e) {
                // omitted exception handling
            }

            return item;  // (7)
        }
    }

表 37. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `FieldSetMapper`クラスを実装し、`mapFieldSet`メソッドをオーバーライドする。 `FieldSetMapper`の型引数には変換対象クラスを設定する。
(2) | 変換処理等を行ったデータを格納するために変換対象クラスの変数を定義する。
(3) | 引数で受けた`FieldSet`から`branchId`を取得し、変換対象クラスの変数へ格納する。 `branchId`は変換処理が不要であるため、変換処理等は行っていない。
(4) | 引数で受けた`FieldSet`から`date`を取得し、変換対象クラスの変数へ格納する。 西暦フォーマットの日付をLocalDate型へ変換するため、`DateTimeFormatter`でフォーマットを指定している。
(5) | 引数で受けた`FieldSet`から`customerId`を取得し、変換対象クラスの変数へ格納する。 `customerId`は変換処理が不要であるため、変換処理等は行っていない。
(6) | 引数で受けた`FieldSet`から`amount`を取得し、変換対象クラスの変数へ格納する。 カンマを含む値をBigDecimal型へ変換するため、`DecimalFormat`を使用している。
(7) | 処理結果を保持している変換対象クラスを返す。

| FieldSetクラスからの値取得 `FieldSet`クラスは、下記のような格納された値を取得するための様々なデータ型に対応したメソッドをもつ。 また、`FieldSet`生成時にフィールドの名前と関連付けられてデータを格納した場合は、名前指定でのデータ取得、名前を指定しない場合ではインデックスを指定してのデータ取得が可能である。 `readString()` `readInt()` `readBigDecimal()` など

### XMLファイル

XMLファイルを扱う場合の定義方法を説明する。

### オブジェクト変換ライブラリ

BeanとXML間の変換処理(O/X (Object/XML) マッピング)にはSpring Frameworkが提供するライブラリを使用する。
XMLファイルとオブジェクト間の変換処理を行うライブラリとして、XStreamやJAXBなどを利用した`Marshaller`および`Unmarshaller`を実装クラスが提供されている。
状況に応じて適しているものを使用すること。

JAXBとXStreamを例に特徴と採用する際のポイントを説明する。

JAXB

  - 変換対象のBeanはBean定義にて指定する

  - スキーマファイルを用いたバリデーションを行うことができる

  - 対外的にスキーマを定義しており、入力ファイルの仕様が厳密に決まっている場合に有用である

XStream

  - Bean定義にて柔軟にXMLの要素とBeanのフィールドをマッピングすることができる

  - 柔軟にBeanマッピングする必要がある場合に有用である

なお、以降の説明ではJAXBを利用する例を示す。

#### 入出力におけるエンコーディングの仕様

XMLの入出力にはSpring Batchが提供する`org.springframework.batch.item.xml.StaxEventItemReader` および`org.springframework.batch.item.xml.StaxEventItemWriter`を使用する。

これらのコンポーネントのエンコーディングのデフォルト値は以下の表のとおり異なっているため、利用時には注意する必要がある。 デフォルト値の違いによって意図しないエンコーディングで入出力が行われることを防ぐため、 デフォルト値をそのまま使用する意図である場合でも明示的にエンコーディングを設定することを推奨する。

表 38. 各コンポーネントのエンコーディングについての仕様
項番 | コンポーネント名 | エンコーディングの指定方法 | デフォルト値
--- | --- | --- | ---
(1) | `StaxEventItemReader` | Bean定義においてencodingプロパティを設定する。 | UTF-8
(2) | `StaxEventItemWriter` | Bean定義においてencodingプロパティを設定する。 | UTF-8

#### 入力

XMLファイルの入力にはSpring Batchが提供する`org.springframework.batch.item.xml.StaxEventItemReader`を使用する。
`StaxEventItemReader`は指定した`Unmarshaller`を使用してXMLファイルをBeanにマッピングすることでXMLファイルを読み込むことができる。

`StaxEventItemReader`は以下の要領で定義する。

- XMLのルート要素となる変換対象クラスに`@XmlRootElement`を付与する

- `StaxEventItemReader`に以下のプロパティを設定する

  - `resource`プロパティに読み込み対象ファイルを設定する

  - `fragmentRootElementName`プロパティにルート要素の名前を設定する

  - `unmarshaller`プロパティに`org.springframework.oxm.jaxb.Jaxb2Marshaller`を設定する

- `Jaxb2Marshaller`には以下のプロパティを設定する

  - `classesToBeBound`プロパティに変換対象のクラスをリスト形式で設定する

  - スキーマファイルを用いたバリデーションを行う場合は、以下に示す2つのプロパティを設定する

    - `schema`プロパティにバリデーションにて使用するスキーマファイルを設定する

    - `validationEventHandler`プロパティにバリデーションにて発生したイベントを処理する`ValidationEventHandler`の実装クラスを設定する

下記の入力ファイルを読み込むための設定例を示す。

依存ライブラリの追加

本ガイドラインの前提においてJAXBを利用する場合は、以下の依存関係が必須である。
アプリケーションの依存ライブラリやバッチから提供されるライブラリにjaxb-implがない場合は、jaxb-implも追加する。

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-oxm</artifactId>
    </dependency>

    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
    </dependency>

    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <scope>runtime</scope> <!-- (1) -->
    </dependency>

表 39. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | jaxb-implはアプリケーションの実行時にのみ利用されるため、スコープはruntimeとする。

入力ファイル例

    <?xml version="1.0" encoding="UTF-8"?>
    <records>
        <customer>
            <name>Data Taro</name>
            <phoneNumbers>
                <phone-number>01234567890</phone-number>
            </phoneNumbers>
        </customer>
        <customer>
            <name>Data Jiro</name>
            <phoneNumbers>
                <phone-number>01234567891</phone-number>
                <phone-number>01234567892</phone-number>
            </phoneNumbers>
        </customer>
        <customer>
            <name>Data Hanako</name>
            <phoneNumbers>
                <phone-number>01234567893</phone-number>
                <phone-number>01234567894</phone-number>
            </phoneNumbers>
        </customer>
    </records>

変換対象クラス

    @XmlRootElement  // (1)
    public class Customer {

        private String name;
        private List<PhoneNumber> phoneNumbers = new ArrayList<>();

        @XmlElement  // (2)
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @XmlElement(name = "phone-number")  // (3)
        @XmlElementWrapper(name = "phoneNumbers")  // (4)
        public List<PhoneNumber> getPhoneNumbers() {
            return phoneNumbers;
        }

        public void setPhoneNumbers(List<PhoneNumber> phoneNumbers) {
            this.phoneNumbers = phoneNumbers;
        }

        // omitted.
    }

    @XmlType(name = "phone-number") 　// (5)
    public class PhoneNumber {

        private String phoneNumber;

        @XmlValue 　// (6)
        public String getPhoneNumber() {
            return phoneNumber;
        }

        public void setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
        }

        // omitted.
    }

表 40. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@XmlRootElement`アノテーションで`<customer>`要素をルート要素としてXML要素にマップする。 XML要素とクラス名が同じの場合、`name`属性での要素の指定は不要である。
(2) | `@XmlElement`アノテーションで`<name>`要素をXML要素にマップする。 XML要素とJavaBeanプロパティが同じの場合、`name`属性での要素の指定は不要である。
(3) | `@XmlElement`アノテーションで`<phone-number>`要素をXML要素にマップする。 `name`属性には、(4)の属性名でラップされる要素の名前を設定する。
(4) | `@XmlElementWrapper`アノテーションで(3)の要素をラップするラッパー要素を生成する。 `name`属性には、ラッパー要素の名前を設定する。
(5) | `@XmlType`アノテーションで(3)の`<phone-number>`要素にクラスをマップする。 `name`属性には、マップされる要素の名前を設定する。
(6) | `@XmlValue`アノテーションで(5)でマップされたクラスの`<phone-number>`要素にマップする。

上記のファイルを読む込むための設定は以下のとおり。

batch-application.properties

    (1)
    #TERASOLUNA FileAccess settings.
    p:schema="file:/usr/local/terasoluna/functionaltest/files/test/input/ch05/fileaccess/customer.xsd"

表 41. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | XMLファイルの入力に使用するスキーマファイルのパスを指定する。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public StaxEventItemReader<Customer> reader(
            @Value("#{jobParameters['inputFile']}") File inputFile, // (1)
            @Value("${fileaccess.schema-file-path}") String schemaFile, // (6)
            CustomerValidationEventHandler customerValidationEventHandler) throws Exception {
        Jaxb2Marshaller unmarshaller = new Jaxb2Marshaller(); // (5)
        unmarshaller.setSchema(new FileSystemResource(schemaFile)); // (6)
        unmarshaller.setValidationEventHandler(customerValidationEventHandler); // (7)
        unmarshaller.setClassesToBeBound(Customer.class); // (8)
        unmarshaller.afterPropertiesSet();
        return new StaxEventItemReaderBuilder<Customer>()
                .name(ClassUtils.getShortName(StaxEventItemReader.class))
                .unmarshaller(unmarshaller)
                .resource(new FileSystemResource(inputFile)) // (1)
                .encoding("UTF-8") // (2)
                .addFragmentRootElements("customer") // (3)
                .strict(true) // (4)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) (4) -->
    <bean id="reader" class="org.springframework.batch.item.xml.StaxEventItemReader" scope="step"
          p:resource="file:#{jobParameters['inputFile']}"
          p:encoding="UTF-8"
          p:fragmentRootElementName="customer"
          p:strict="true">
        <property name="unmarshaller">  <!-- (5) -->
            <!-- (6) (7) -->
            <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller"
                  p:schema="file:${fileaccess.schema-file-path}"
                  p:validationEventHandler-ref="customerValidationEventHandler">
                <property name="classesToBeBound">  <!-- (8) -->
                    <list>
                        <value>org.terasoluna.batch.functionaltest.ch05.fileaccess.model.jaxb.Customer</value>
                    </list>
                </property>
            </bean>
        </property>
    </bean>

表 42. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 入力ファイルを設定する。 |  | なし
(2) | encoding | 入力ファイルのエンコーディングを設定する。 入出力におけるエンコーディングの仕様で述べたように、ItemReader/Writer間のデフォルト値の違いによって意図しないエンコーディングで 入力が行われることを防ぐため、デフォルト値をそのまま使用する意図である場合でも明示的にエンコーディングを設定することを推奨する。 |  | JavaVMのデフォルトエンコーディング
(3) | fragmentRootElementName | ルート要素の名前を設定する。 対象となるオブジェクトが複数ある場合には、`fragmentRootElementNames`を利用する。 |  | なし
(4) | strict | trueを設定すると、入力ファイルが存在しない(開けない)場合に例外が発生する。 |  | true
(5) | unmarshaller | アンマーシャラを設定する。 JAXBを利用する場合は、`org.springframework.oxm.jaxb.Jaxb2Marshaller`のBeanを設定する。 |  | なし
(6) | schema | バリデーションにて使用するスキーマファイルを設定する。 この例では、スキーマファイルの場所を`batch-application.properties`に定義し、`@Value`を使用してプロパティとしてインジェクションする方式とする。 これ以外にもファイルの場所の指定方法はいくつかのバリエーションが考えられるが、詳細説明は割愛する（ファイルパスではなくクラスパスで指定する、パス情報をプロパティファイルに定義する、等）。 開発案件のルールやアプリケーション実行環境を考慮して最適な方式を検討すること。 スキーマファイルの記述例は後述する。 |  |
(7) | validationEventHandler | バリデーションにて発生したイベントを処理する`ValidationEventHandler`の実装クラスを設定する。 `ValidationEventHandler`の実装例は後述する。 |  |
(8) | classesToBeBound | 変換対象のクラスをリスト形式で設定する。 |  | なし

スキーマファイル記述例

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <!-- (1) -->
        <xs:element name="customer">
            <!-- (2) -->
            <xs:complexType>
                <!-- (3) -->
                <xs:sequence>
                    <!-- (4) -->
                    <xs:element name="name" type="stringMaxSize"/>  <!-- (5) -->
                    <xs:element name="phoneNumbers" type="phoneNumberList"/>  <!-- (6) -->
                </xs:sequence>
            </xs:complexType>
        </xs:element>

        <!-- (7) -->
        <xs:simpleType name="stringMaxSize">
            <xs:restriction base="xs:string">
                <xs:maxLength value="10"/>
            </xs:restriction>
        </xs:simpleType>

        <!-- (8) -->
        <xs:complexType name="phoneNumberList">
            <xs:sequence>
                <xs:element name="phone-number" minOccurs="1" maxOccurs="2"/>
            </xs:sequence>
        </xs:complexType>

    </xs:schema>

表 43. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `<xs:element>`要素の`name`属性には、入力XMLファイルのレコードにおけるルート要素を設定する。 ここでは`customer`を設定する。
(2) | `<xs:complexType>`要素は、子要素や属性を設定する。
(3) | `<xs:sequence>`要素は、子要素の出現順序を設定する。 ここでは、`name`、`phoneNumbers`の順で出現することを設定する。
(4) | `<xs:element>`要素は、子要素の属性名や属性値のデータ型などを設定する。
(5) | 子要素`name`に(7)の制約を設定する。
(6) | 子要素`phoneNumbers`に(8)の制約を設定する。
(7) | `<xs:simpleType>`要素は、整数値の範囲や文字列の長さといった制約を設定することができる。 ここでは、文字列の長さの最大値は10という制約を設定する。
(8) | `phoneNumbers`要素の子要素である`phone-number`要素に制約を設定する。 ここでは、最低出現回数が1回、最高出現回数が2回という制約を設定する。

ValidationEventHandlerの実装例

    @Component
    // (1)
    public class CustomerValidationEventHandler implements ValidationEventHandler {
        /**
         * Logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(CustomerValidationEventHandler.class);

        @Override
        public boolean handleEvent(ValidationEvent event) {
            // (2)
            logger.error("[EVENT [SEVERITY:{}] [MESSAGE:{}] [LINKED EXCEPTION:{}] [LOCATOR: " +
                    "[LINE NUMBER:{}] [COLUMN NUMBER:{}] [OFFSET:{}] [OBJECT:{}] [NODE:{}] [URL:{}] ] ]",
                    event.getSeverity(),
                    event.getMessage(),
                    event.getLinkedException(),
                    event.getLocator().getLineNumber(),
                    event.getLocator().getColumnNumber(),
                    event.getLocator().getOffset(),
                    event.getLocator().getObject(),
                    event.getLocator().getNode(),
                    event.getLocator().getURL());
            return false; // (3)
        }
    }

表 44. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ValidationEventHandler`クラスを実装し、`handleEvent`メソッドをオーバーライドする。
(2) | 引数で受けたevent(`ValidationEvent`)からイベントの情報を取得し、適宜処理を行う。 例ではイベントの情報をログ出力している。
(3) | 検証処理を終了させるためfalseを返す。 検証処理を続行する場合はtrueを返す。 適切な`UnmarshalException`、`ValidationException`、または`MarshalException`を生成して現在の操作を終了させる場合はfalseを返す。

#### 出力

XMLファイルの出力にはSpring Batchが提供する`org.springframework.batch.item.xml.StaxEventItemWriter`を使用する。
`StaxEventItemWriter`は指定した`Marshaller`を使用してBeanをXMLにマッピングすることでXMLファイルを出力することができる。

`StaxEventItemWriter`は以下の要領で定義する。

- 変換対象クラスに以下の設定を行う

  - XMLのルート要素となる変換対象クラスに`@XmlRootElement`を付与する

  - `@XmlType`アノテーションを使用してフィールドを出力する順番を設定する

  - XMLへの変換対象外とするフィールドがある場合、対象フィールドのgetterに`@XmlTransient`アノテーションを付与する

- `StaxEventItemWriter`に以下のプロパティを設定する

  - `resource`プロパティに出力対象ファイルを設定する

  - `marshaller`プロパティに`org.springframework.oxm.jaxb.Jaxb2Marshaller`を設定する

- `Jaxb2Marshaller`には以下のプロパティを設定する

  - `classesToBeBound`プロパティに変換対象のクラスをリスト形式で設定する

後述で示される出力ファイル例を書き出すための設定例を示す。

依存ライブラリの追加

依存ライブラリを追加する際の設定方法は入力で説明されている依存ライブラリの追加を参照する。

XMLファイル出力時のフォーマット処理(改行およびインデント)について

`Jaxb2Marshaller` のデフォルト設定では、XMLファイルはフォーマット処理(改行およびインデント)が行われないまま出力される。
後述の出力ファイル例で示されているように、XMLのフォーマット処理を行うためには、以下のように `Jaxb2Marshaller` に対して `marshallerProperties` を設定する必要がある。

JavaConfig

    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setClassesToBeBound(CustomerToJaxb.class);
    marshaller.setMarshallerProperties(Map.of("javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT", true));
    marshaller.afterPropertiesSet();

XMLConfig

    <property name="marshaller">
        <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
            <property name="classesToBeBound">
                <!-- omitted settings -->
            </property>
            <property name="marshallerProperties">
                <map>
                    <entry>
                        <key>
                            <util:constant
                                static-field="javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT"/>
                        </key>
                        <value type="java.lang.Boolean">true</value>
                    </entry>
                </map>
            </property>
        </bean>
    </property>

変換対象クラス

    @XmlRootElement(name = "Customer")  // (1)
    @XmlType(propOrder={"customerId", "customerName", "customerAddress",
            "customerTel", "chargeBranchId"})  // (2)
    public class CustomerToJaxb {

        private String customerId;
        private String customerName;
        private String customerAddress;
        private String customerTel;
        private String chargeBranchId;
        private LocalDateTime createDate;
        private LocalDateTime updateDate;

        // omitted getter/setter

        @XmlTransient  // (3)
        public LocalDateTime getCreateDate() { return createDate; }

        @XmlTransient  // (3)
        public LocalDateTime getUpdateDate() { return updateDate; }
    }

表 45. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `@XmlRootElement`アノテーションで`<Customer>`要素をルート要素としてXML要素にマップする。 `name`属性には、`Customer`を設定する。
(2) | `@XmlType`アノテーションを使用してフィールドを出力する順番を設定する。
(3) | XMLへの変換対象外とするフィールドのgetterに`@XmlTransient`アノテーションを付与する。

後述の出力ファイル例を書き出すための設定は以下のとおり。

JavaConfig
Bean定義

    @Bean
    @StepScope
    public StaxEventItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) throws Exception {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); // (8)
        marshaller.setClassesToBeBound(CustomerToJaxb.class); // (9)
        marshaller.setMarshallerProperties(Map.of("javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT", true)); // (10)
        marshaller.afterPropertiesSet();
        return new StaxEventItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(StaxEventItemWriter.class))
                .resource(new FileSystemResource(outputFile)) // (1)
                .encoding("MS932") // (2)
                .rootTagName("records") // (3)
                .overwriteOutput(true) // (4)
                .shouldDeleteIfEmpty(false) // (5)
                .transactional(true) // (6)
                .standalone(false) // (7)
                .marshaller(marshaller)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) (3) (4) (5) (6) (7) -->
    <bean id="writer"
          class="org.springframework.batch.item.xml.StaxEventItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:encoding="MS932"
          p:rootTagName="records"
          p:overwriteOutput="true"
          p:shouldDeleteIfEmpty="false"
          p:transactional="true"
          p:standalone="false">
        <property name="marshaller">  <!-- (8) -->
            <bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
                <property name="classesToBeBound">  <!-- (9) -->
                    <list>
                        <value>org.terasoluna.batch.functionaltest.ch05.fileaccess.model.mst.CustomerToJaxb</value>
                    </list>
                </property>
                <property name="marshallerProperties">
                    <map>
                        <entry>
                            <key>
                                <util:constant
                                    static-field="javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT"/> <!-- (10) -->
                            </key>
                            <value type="java.lang.Boolean">true</value>
                        </entry>
                    </map>
                </property>
            </bean>
        </property>
    </bean>

表 46. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | resource | 出力ファイルを設定する。 |  | なし
(2) | encoding | 出力ファイルのエンコーディングを設定する。 未指定の場合のデフォルト値は「UTF-8」。 |  | UTF-8
(3) | rootTagName | ルート要素の名前を設定する。 |  |
(4) | overwriteOutput | trueの場合、既にファイルが存在すれば削除する。 falseの場合、既にファイルが存在すれば例外をスローする。 |  | true
(5) | shouldDeleteIfEmpty | 出力件数が0件であった場合にファイルを削除するかを設定する。 trueの場合、ファイル出力後に出力件数が0件であれば出力対象ファイルを削除する。 他の設定との組み合わせによって意図しない動作をする場合がある。詳細はFlatFileItemWriterのshouldDeleteIfExistsとshouldDeleteIfEmptyの動作の違いを参照。 |  | false
(6) | transactional | トランザクション制御を行うかを設定する。詳細は、トランザクション制御を参照。 |  | true
(7) | standalone | 出力ファイルのstandalone属性を設定する。 |  | なし
(8) | marshaller | マーシャラを設定する。 JAXBを利用する場合は、`org.springframework.oxm.jaxb.Jaxb2Marshaller`を設定する。 |  | なし
(9) | classesToBeBound | 変換対象のクラスをリスト形式で設定する。 |  | なし
(10) | marshallerProperties | JAXB Marshaller プロパティを設定する。 ここでは、フォーマット済みの出力を行うために`javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT`をtrueに設定している。 |  | なし

##### ヘッダ・フッタの出力

ヘッダとフッタの出力には、`org.springframework.batch.item.xml.StaxWriterCallback`の実装クラスを使用する。

ヘッダの出力は、`headerCallback`、フッタの出力は、`footerCallback`に`StaxWriterCallback`の実装を設定する。

ヘッダはルート要素の開始タグ直後、フッタはルート要素の終了タグ直前に出力される。

`StaxEventItemWriter` のBean定義を以下に示す。

JavaConfig
Bean定義

    public StaxEventItemWriter<Customer> writer(
            @Value("#{jobParameters['outputFile']}") File outputFile,
            WriteHeaderStaxWriterCallback writeHeaderStaxWriterCallback,
            WriteFooterStaxWriterCallback writeFooterStaxWriterCallback) throws Exception {
        // ommited settings
        return new StaxEventItemWriterBuilder<Customer>()
                .name(ClassUtils.getShortName(StaxEventItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .headerCallback(writeHeaderStaxWriterCallback) // (1)
                .footerCallback(writeFooterStaxWriterCallback) // (2)
                .marshaller(marshaller)
                .build();
    }

XMLConfig
Bean定義

    <!-- (1) (2) -->
    <bean id="writer"
          class="org.springframework.batch.item.xml.StaxEventItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"
          p:headerCallback-ref="writeHeaderStaxWriterCallback"
          p:footerCallback-ref="writeFooterStaxWriterCallback">
        <property name="marshaller">
            <!-- omitted settings -->
        </property>
    </bean>

表 47. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | headerCallback | `StaxWriterCallback`の実装クラスを設定する。 |  |
(2) | footerCallback | `StaxWriterCallback`の実装クラスを設定する。 |  |

`StaxWriterCallback`は以下の要領で実装する。

- `StaxWriterCallback`クラスを実装し、`write`メソッドをオーバーライドする

- 引数で受ける`XMLEventWriter`を用いてヘッダ/フッタを出力する

StaxWriterCallbackの実装例

    @Component
    public class WriteHeaderStaxWriterCallback implements StaxWriterCallback { // (1)
        @Override
        public void write(XMLEventWriter writer) throws IOException {
            XMLEventFactory factory = XMLEventFactory.newInstance();
            try {
                writer.add(factory.createComment(" Customer list header ")); // (2)
            } catch (XMLStreamException e) {
                // omitted exception handling
            }
        }
    }

表 48. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `StaxWriterCallback`クラスを実装し、`write`メソッドをオーバーライドする。
(2) | 引数で受ける`XMLEventWriter`を用いてヘッダ/フッタを出力する。

| XMLEventFactoryを使用したXMLの出力 `XMLEventWriter`クラスを用いたXMLファイルの出力では`XMLEventFactory`クラスを使用することで効率的に`XMLEvent`を生成することができる。 `XMLEventWriter`クラスには`add`メソッドが定義されており、`XMLEvent`オブジェクトを引数に取りXMLファイルの出力を行う。 `XMLEvent`オブジェクトを都度生成するのは非常に手間が掛かるため、`XMLEvent`を容易に生成することができる`XMLEventFactory`クラスを使用する。 `XMLEventFactory`クラスには `createStartDocument`メソッドや`createStartElement`メソッドなど、作成するイベントに対応したメソッドが定義してある。

下記はXMLファイルの出力でStaxEventItemWriterを使用したXMLファイルの出力例である。

出力ファイル例

    <?xml version="1.0" encoding="UTF-8"?>
    <records>
    <!-- Customer list header -->
      <Customer>
        <customerId>001</customerId>
        <customerName>CustomerName001</customerName>
        <customerAddress>CustomerAddress001</customerAddress>
        <customerTel>11111111111</customerTel>
        <chargeBranchId>001</chargeBranchId></Customer>
      <Customer>
        <customerId>002</customerId>
        <customerName>CustomerName002</customerName>
        <customerAddress>CustomerAddress002</customerAddress>
        <customerTel>11111111111</customerTel>
        <chargeBranchId>002</chargeBranchId></Customer>
      <Customer>
        <customerId>003</customerId>
        <customerName>CustomerName003</customerName>
        <customerAddress>CustomerAddress003</customerAddress>
        <customerTel>11111111111</customerTel>
        <chargeBranchId>003</chargeBranchId>
      </Customer>
    <!-- Customer list footer -->
    </records>

### マルチフォーマット

マルチフォーマットファイルを扱う場合の定義方法を説明する。

マルチフォーマットは、Overviewで説明したとおり(ヘッダn行 + データn行 + トレーラn行)* n + フッタn行 の形式を基本とするが以下のようなパターンも存在する。

- フッタレコードがある場合、ない場合

- 同一レコード区分内でフォーマットが異なるレコードがある場合

  - 例)データ部は項目数が5と6のデータレコードが混在する

マルチフォーマットのパターンはいくつかあるが、実現方式は同じになる。

#### 入力

マルチフォーマットファイルの読み込みには、Spring Batchが提供する`org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper`を使用する。
マルチフォーマットファイルでは各レコードのフォーマットごとに異なるBeanにマッピングする必要がある。
`PatternMatchingCompositeLineMapper`は、パターンマッチによってレコードに対して使用する`LineTokenizer`および`FieldSetMapper`を選択することができる。

たとえば、以下のような形で使用する`LineTokenizer`を選択することが可能である。

- `USER*`にマッチする(レコードの先頭が`USER`である)場合は`userTokenizer`を使用する

- `LINEA*`にマッチする(レコードの先頭が`LINEA`である)場合は`lineATokenizer`を使用する

| マルチフォーマットファイルを読み込む際のレコードにかかるフォーマットの制約 マルチフォーマットファイルを読み込むためには、レコード区分がAntPathMatcherによるパターンマッチで判別可能なフォーマットでなければならない。 詳細は`PatternMatcher`のAPIドキュメントを参照。

`PatternMatchingCompositeLineMapper`は以下の要領で実装する。

- 変換対象クラスはレコード区分をもつクラスを定義し、各レコード区分のクラスに継承させる

- 各レコードをBeanにマッピングするための`LineTokenizer`および`FieldSetMapper`を定義する

- `PatternMatchingCompositeLineMapper`を定義する

  - `tokenizers`プロパティに各レコード区分に対応する`LineTokenizer`を設定する

  - `fieldSetMappers`プロパティに各レコード区分に対応する`FieldSetMapper`を設定する

| 変換対象クラスはレコード区分をもつクラスを定義し、各レコード区分のクラスに継承させる `ItemProcessor`は1つの型を引数に取る仕様である。 しかし、単純に`PatternMatchingCompositeLineMapper`にてマルチフォーマットのファイルをレコード区分ごとに異なるBeanにマッピングすると、`ItemProcessor`は1つの型を引数に取るため複数の型を処理することができない。 そのため、変換対象のクラスに継承関係をもたせ、`ItemProcessor`の引数の型にスーパークラスを指定することで解決が可能である。 以下に変換対象クラスのクラス図と`ItemProcessor`の定義例を示す。 図 6. 変換対象クラスのクラス図 ItemProcessorの定義例
    public class MultiFormatItemProcessor implements
            ItemProcessor<SalesPlanDetailMultiFormatRecord, String> {
        @Override
        // (1)
        public String process(SalesPlanDetailMultiFormatRecord item) throws Exception {
            String record = item.getRecord();  // (2)

            switch (record) {  // (3)
            case "H":
                // omitted business logic
            case "D":
                // omitted business logic
            case "T":
                // omitted business logic
            case "E":
                // omitted business logic
            default:
                // omitted exception handling
            }
        }
    }
表 49. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | `ItemProcessor`の引数に継承関係をもたせた変換対象クラスのスーパークラスを設定する。
(2) | itemからレコード区分を取得する。 各レコード区分によって実態のクラスは異なるが、ポリモーフィズムによってレコード区分を取得できる。
(3) | レコード区分を判定し、各レコード区分ごとの処理を行う。 適宜、クラスの変換処理等を行うこと。

以下に下記の入力ファイルを読み込むための設定例を示す。実装例を示す。

入力ファイル例

    H,Sales_plan_detail header No.1
    D,000001,2016,1,0000000001,100000000
    D,000001,2016,1,0000000002,200000000
    D,000001,2016,1,0000000003,300000000
    T,000001,3,600000000
    H,Sales_plan_detail header No.2
    D,00002,2016,1,0000000004,400000000
    D,00002,2016,1,0000000005,500000000
    D,00002,2016,1,0000000006,600000000
    T,00002,3,1500000000
    H,Sales_plan_detail header No.3
    D,00003,2016,1,0000000007,700000000
    D,00003,2016,1,0000000008,800000000
    D,00003,2016,1,0000000009,900000000
    T,00003,3,2400000000
    E,3,9,4500000000

下記に変換対象クラスのBean定義例を示す。

変換対象クラス

    /**
     * Model of record indicator of sales plan detail.
     */
    public class SalesPlanDetailMultiFormatRecord {

        protected String record;

        // omitted getter/setter
    }

    /**
     * Model of sales plan detail header.
     */
    public class SalesPlanDetailHeader extends SalesPlanDetailMultiFormatRecord {

        private String description;

        // omitted getter/setter
    }

    /**
     * Model of Sales plan Detail.
     */
    public class SalesPlanDetailData extends SalesPlanDetailMultiFormatRecord {

        private String branchId;
        private int year;
        private int month;
        private String customerId;
        private BigDecimal amount;

        // omitted getter/setter
    }

    /**
     * Model of Sales plan Detail.
     */
    public class SalesPlanDetailTrailer extends SalesPlanDetailMultiFormatRecord {

        private String branchId;
        private int number;
        private BigDecimal total;

        // omitted getter/setter
    }

    /**
     * Model of Sales plan Detail.
     */
    public class SalesPlanDetailEnd extends SalesPlanDetailMultiFormatRecord {
        // omitted getter/setter

        private int headNum;
        private int trailerNum;
        private BigDecimal total;

        // omitted getter/setter
    }

上記のファイルを読む込むための設定は以下のとおり。

JavaConfig
Bean定義例

    // (1)
    @Bean
    public DelimitedLineTokenizer headerDelimitedLineTokenizer() {
        DelimitedLineTokenizer headerDelimitedLineTokenizer = new DelimitedLineTokenizer();
        headerDelimitedLineTokenizer.setNames("record", "description");
        return headerDelimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer dataDelimitedLineTokenizer() {
        DelimitedLineTokenizer dataDelimitedLineTokenizer = new DelimitedLineTokenizer();
        dataDelimitedLineTokenizer.setNames("record", "branchId", "year", "month", "customerId", "amount");
        return dataDelimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer trailerDelimitedLineTokenizer() {
        DelimitedLineTokenizer trailerDelimitedLineTokenizer = new DelimitedLineTokenizer();
        trailerDelimitedLineTokenizer.setNames("record", "branchId", "number", "total");
        return trailerDelimitedLineTokenizer;
    }

    @Bean
    public DelimitedLineTokenizer endDelimitedLineTokenizer() {
        DelimitedLineTokenizer endDelimitedLineTokenizer = new DelimitedLineTokenizer();
        endDelimitedLineTokenizer.setNames("record", "headNum", "trailerNum", "total");
        return endDelimitedLineTokenizer;
    }

    // (2)
    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailHeader> headerBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailHeader> headerBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        headerBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailHeader.class);
        return headerBeanWrapperFieldSetMapper;
    }

    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailData> dataBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailData> dataBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        dataBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailData.class);
        return dataBeanWrapperFieldSetMapper;
    }

    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailTrailer> trailerBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailTrailer> trailerBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        trailerBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailTrailer.class);
        return trailerBeanWrapperFieldSetMapper;
    }

    @Bean
    public BeanWrapperFieldSetMapper<SalesPlanDetailEnd> endBeanWrapperFieldSetMapper() {
        BeanWrapperFieldSetMapper<SalesPlanDetailEnd> endBeanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        endBeanWrapperFieldSetMapper.setTargetType(SalesPlanDetailEnd.class);
        return endBeanWrapperFieldSetMapper;
    }

    @Bean
    @StepScope
    public FlatFileItemReader reader(
            @Value("#{jobParameters['inputFile']}") File inputFile,
            LineTokenizer headerDelimitedLineTokenizer,
            LineTokenizer dataDelimitedLineTokenizer,
            LineTokenizer trailerDelimitedLineTokenizer,
            LineTokenizer endDelimitedLineTokenizer,
            FieldSetMapper<SalesPlanDetailHeader> headerBeanWrapperFieldSetMapper,
            FieldSetMapper<SalesPlanDetailData> dataBeanWrapperFieldSetMapper,
            FieldSetMapper<SalesPlanDetailTrailer> trailerBeanWrapperFieldSetMapper,
            FieldSetMapper<SalesPlanDetailEnd> endBeanWrapperFieldSetMapper) {

        Map<String, LineTokenizer> tokenizers = new HashMap<>(); // (4)
        tokenizers.put("H*", headerDelimitedLineTokenizer);
        tokenizers.put("D*", dataDelimitedLineTokenizer);
        tokenizers.put("T*", trailerDelimitedLineTokenizer);
        tokenizers.put("E*", endDelimitedLineTokenizer);

        Map<String, FieldSetMapper> fieldSetMappers = new HashMap<>(); // (5)
        fieldSetMappers.put("H*", headerBeanWrapperFieldSetMapper);
        fieldSetMappers.put("D*", dataBeanWrapperFieldSetMapper);
        fieldSetMappers.put("T*", trailerBeanWrapperFieldSetMapper);
        fieldSetMappers.put("E*", endBeanWrapperFieldSetMapper);

        final PatternMatchingCompositeLineMapper lineMapper = new PatternMatchingCompositeLineMapper(); // (3)
        lineMapper.setTokenizers(tokenizers);
        lineMapper.setFieldSetMappers(fieldSetMappers);

        final FlatFileItemReader reader = new FlatFileItemReader();
        reader.setResource(new FileSystemResource(inputFile));
        reader.setLineMapper(lineMapper);
        return reader;
    }

XMLConfig
Bean定義例

    <!-- (1) -->
    <bean id="headerDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,description"/>

    <bean id="dataDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,branchId,year,month,customerId,amount"/>

    <bean id="trailerDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,branchId,number,total"/>

    <bean id="endDelimitedLineTokenizer"
          class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"
          p:names="record,headNum,trailerNum,total"/>

    <!-- (2) -->
    <bean id="headerBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="org.terasoluna.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailHeader"/>

    <bean id="dataBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="org.terasoluna.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailData"/>

    <bean id="trailerBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="org.terasoluna.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailTrailer"/>

    <bean id="endBeanWrapperFieldSetMapper"
          class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"
          p:targetType="org.terasoluna.batch.functionaltest.ch05.fileaccess.model.plan.SalesPlanDetailEnd"/>

    <bean id="reader"
        class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"
        p:resource="file:#{jobParameters['inputFile']}">
        <property name="lineMapper">  <!-- (3) -->
            <bean class="org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper">
                <property name="tokenizers">  <!-- (4) -->
                    <map>
                        <entry key="H*" value-ref="headerDelimitedLineTokenizer"/>
                        <entry key="D*" value-ref="dataDelimitedLineTokenizer"/>
                        <entry key="T*" value-ref="trailerDelimitedLineTokenizer"/>
                        <entry key="E*" value-ref="endDelimitedLineTokenizer"/>
                    </map>
                </property>
                <property name="fieldSetMappers">  <!-- (5) -->
                    <map>
                        <entry key="H*" value-ref="headerBeanWrapperFieldSetMapper"/>
                        <entry key="D*" value-ref="dataBeanWrapperFieldSetMapper"/>
                        <entry key="T*" value-ref="trailerBeanWrapperFieldSetMapper"/>
                        <entry key="E*" value-ref="endBeanWrapperFieldSetMapper"/>
                    </map>
                </property>
            </bean>
        </property>
    </bean>

表 50. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | 各レコードに対応する`LineTokenizer` | 各レコード区分に対応する`LineTokenizer`を定義する。 |  |
(2) | 各レコードに対応する`FieldSetMapper` | 各レコード区分に対応する`FieldSetMapper`を定義する。 |  |
(3) | lineMapper | `org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper`を設定する。 |  | なし
(4) | tokenizers | map形式で各レコード区分に対応する`LineTokenizer`を設定する。 `key`にレコードを判別するパターンを設定し、`value-ref`に使用する`LineTokenizer`を設定する。 |  | なし
(5) | fieldSetMappers | map形式で各レコード区分に対応する`FieldSetMapper`を設定する。 `key`にレコードを判別するパターンを設定し、`value-ref`に使用する`FieldSetMapper`を設定する。 |  | なし

#### 出力

マルチフォーマットファイルを扱う場合の定義方法を説明する。

マルチフォーマットファイル読み込みではレコード区分によって使用する`LineTokenizer`および`FieldSetMapper`を判別する`PatternMatchingCompositeLineMapper`を使用することで実現可能である。
しかし、書き込み時に同様の機能をもつコンポーネントは提供されていない。

そのため、`ItemProcessor`内で変換対象クラスをレコード(文字列)に変換する処理までを行い、`ItemWriter`では受け取った文字列をそのまま書き込みを行うことでマルチフォーマットファイルの書き込みを実現する。

マルチフォーマットファイルの書き込みは以下の要領で実装する。

- `ItemProcessor`にて変換対象クラスをレコード(文字列)に変換して`ItemWriter`に渡す

  - 例では、各レコード区分ごとの`LineAggregator`および`FieldExtractor`を定義し、`ItemProcessor`でインジェクトして使用する

- `ItemWriter`では受け取った文字列をそのままファイルへ書き込みを行う

  - `ItemWriter`の`lineAggregator`プロパティに`PassThroughLineAggregator`を設定する

  - `PassThroughLineAggregator`は受け取ったitemの`item.toString()`した結果を返す`LineAggregator`である

以下に下記の出力ファイルを書き出すための設定例を示す。実装例を示す。

出力ファイル例

    H,Sales_plan_detail header No.1
    D,000001,2016,1,0000000001,100000000
    D,000001,2016,1,0000000002,200000000
    D,000001,2016,1,0000000003,300000000
    T,000001,3,600000000
    H,Sales_plan_detail header No.2
    D,00002,2016,1,0000000004,400000000
    D,00002,2016,1,0000000005,500000000
    D,00002,2016,1,0000000006,600000000
    T,00002,3,1500000000
    H,Sales_plan_detail header No.3
    D,00003,2016,1,0000000007,700000000
    D,00003,2016,1,0000000008,800000000
    D,00003,2016,1,0000000009,900000000
    T,00003,3,2400000000
    E,3,9,4500000000

変換対象クラスの定義および`ItemProcessor`定義例、注意点はマルチフォーマットの入力と同様である。

上記のファイルを出力するための設定は以下のとおり。 `ItemProcessor`の定義例をBean定義例の後に示す。

JavaConfig
Bean定義例

    // (1)
    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> headerDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> headerDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "description"});
        headerDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return headerDelimitedLineAggregator;
    }

    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> dataDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> dataDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "branchId", "year", "month", "customerId", "amount"});
        dataDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return dataDelimitedLineAggregator;
    }

    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> trailerDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> trailerDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "branchId", "number", "total"});
        trailerDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return trailerDelimitedLineAggregator;
    }

    @Bean
    public DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> endDelimitedLineAggregator() {
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> endDelimitedLineAggregator = new DelimitedLineAggregator<>();
        BeanWrapperFieldExtractor<SalesPlanDetailMultiFormatRecord> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[] {"record", "headNum", "trailerNum", "total"});
        endDelimitedLineAggregator.setFieldExtractor(fieldExtractor);
        return endDelimitedLineAggregator;
    }

    @Bean
    @StepScope
    public FlatFileItemWriter writer(
            @Value("#{jobParameters['outputFile']}") File outputFile) {
        PassThroughLineAggregator lineAggregator = new PassThroughLineAggregator(); // (2)
        return new FlatFileItemWriterBuilder<SalesPlanDetail>()
                .name(ClassUtils.getShortName(FlatFileItemWriter.class))
                .resource(new FileSystemResource(outputFile))
                .lineAggregator(lineAggregator)
                .build();
    }

XMLConfig
Bean定義例

    <!-- (1) -->
    <bean id="headerDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,description"/>
        </property>
    </bean>

    <bean id="dataDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,branchId,year,month,customerId,amount"/>
        </property>
    </bean>

    <bean id="trailerDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,branchId,number,total"/>
        </property>
    </bean>

    <bean id="endDelimitedLineAggregator"
          class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
        <property name="fieldExtractor">
            <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"
                  p:names="record,headNum,trailerNum,total"/>
        </property>
    </bean>

    <bean id="writer" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"
          p:resource="file:#{jobParameters['outputFile']}"/>
        <property name="lineAggregator">  <!-- (2) -->
            <bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
        </property>
    </bean>

表 51. 設定内容の項目一覧
項番 | 設定項目名 | 設定内容 | 必須 | デフォルト値
--- | --- | --- | --- | ---
(1) | 各レコード区分に対応する`LineAggregator`および`FieldExtractor` | `LineAggregator`および`FieldExtractor`を定義する。 `ItemProcessor`で`LineAggregator`をインジェクトして使用する。 |  |
(2) | lineAggregator | `org.springframework.batch.item.file.transform.PassThroughLineAggregator`を設定する。 |  | なし

`ItemProcessor`の実装例を以下に示す。
例で実装しているのは、受け取ったitemを文字列に変換して`ItemWriter`に渡す処理のみである。

ItemProcessorの定義例

    public class MultiFormatItemProcessor implements
            ItemProcessor<SalesPlanDetailMultiFormatRecord, String> {

        // (1)
        @Inject
        @Named("headerDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> headerDelimitedLineAggregator;

        @Inject
        @Named("dataDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> dataDelimitedLineAggregator;

        @Inject
        @Named("trailerDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> trailerDelimitedLineAggregator;

        @Inject
        @Named("endDelimitedLineAggregator")
        DelimitedLineAggregator<SalesPlanDetailMultiFormatRecord> endDelimitedLineAggregator;

        @Override
        // (2)
        public String process(SalesPlanDetailMultiFormatRecord item) throws Exception {
            String record = item.getRecord();  // (3)

            switch (record) {  // (4)
            case "H":
                return headerDelimitedLineAggregator.aggregate(item);  // (5)
            case "D":
                return dataDelimitedLineAggregator.aggregate(item);  // (5)
            case "T":
                return trailerDelimitedLineAggregator.aggregate(item);  // (5)
            case "E":
                return endDelimitedLineAggregator.aggregate(item);  // (5)
            default:
                throw new IncorrectRecordClassificationException(
                        "Record classification is incorrect.[value:" + record + "]");
            }
        }
    }

表 52. 設定内容の項目一覧
項番 | 説明
--- | ---
(1) | 各レコード区分に対応する`LineAggregator`をインジェクトする。
(2) | `ItemProcessor`の引数に継承関係をもたせた変換対象クラスのスーパークラスを設定する。
(3) | itemからレコード区分を取得する。
(4) | レコード区分を判定し、各レコード区分ごとの処理を行う。
(5) | 各レコード区分に対応する`LineAggregator`を使用し変換対象クラスをレコード(文字列)に変換して`ItemWriter`に渡す。

TERASOLUNA Batch Framework for Java (5.x) Development Guideline - version 5.7.1.RELEASE, 2026-3-31, commit-id:4e0b014
> INDEX