OpenCSVの使用方法について（データクラスに配列やリストがある場合）

# # 初めに
本記事は以前投稿した「OpenCSVの使用方法について」（[下記記事](https://qiita.com/T-H9703EnAc/items/0cbb28935ca7a7e30d9c)）の内容のデータクラス(この記事ではJavaBeans)を使用した際のCSVの書込み方法の続きのようなものです。以前のものだとデータクラスのフィールドの型が配列やリストの場合に上手く出力されないのでそれに対応した形となっております。

https://qiita.com/T-H9703EnAc/items/0cbb28935ca7a7e30d9c

# # 今回作成したプログラムの前提事項
・データクラスの型はすべてString型であること前提とします。
・配列やリストの最大値の設定をアノテーションを用いて行うが出力はその最大値分必ず出力されます。
・配列やリストのヘッダー名は「共通の名前 + 末尾に番号」がついたものになります。

# 配列やリストの場合にそのサイズの最大値を設定するアノテーションを作成する
見出しの通り、配列やリストの場合にそのサイズの最大値を設定するアノテーションを作成します。
```:CsvMultipleDataMaxSize.java
package com.app.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 配列またはリストの最大のサイズを指定する
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.TYPE,
    ElementType.FIELD,
    ElementType.CONSTRUCTOR,
    ElementType.METHOD
})
public @interface CsvMultipleDataMaxSize {
	int maxSize();
}

```

アノテーションの作り方については以下の記事などをごらんください。

https://qiita.com/yasushi-jp/items/fca645778d44fd228864

https://qiita.com/oden141/items/49caccbf72589f3c47e9

https://workteria.forward-soft.co.jp/blog/detail/10179


使用方法は以下のような感じです、
「__@CsvMultipleDataMaxSize(maxSize = 3)__」
```:CSVData1Bean.java
package com.app.bean;

import java.util.List;

import com.app.annotations.CsvMultipleDataMaxSize;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvBindByPosition;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CSVData1Bean {
	@CsvBindByPosition(position = 0)
	@CsvBindByName(column = "エリア", required = true)
	private String area;
	@CsvBindByPosition(position = 1)
	@CsvBindByName(column = "ID", required = true)
	private String id;
	@CsvBindByPosition(position = 2)
	@CsvBindByName(column = "年月日", required = true)
	private String nenTukiHi;
	
	@CsvBindByPosition(position = 3)
	@CsvBindByName(column = "listData", required = true)
	@CsvMultipleDataMaxSize(maxSize = 3)
	private List<String> listData;
	
	@CsvBindByPosition(position = 4)
	@CsvBindByName(column = "arrayData", required = true)
	@CsvMultipleDataMaxSize(maxSize = 3)
	private String[] arrayData;
}

```


# # CustomMappingStrategy.javaの作成
[以前の記事](https://qiita.com/T-H9703EnAc/items/0cbb28935ca7a7e30d9c)で作成した「__CustomMappingStrategy.java__」を大きく変更しております。
```:CustomMappingStrategy.java
package com.app.util;

import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

import com.app.annotations.CsvMultipleDataMaxSize;
import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvChainedException;
import com.opencsv.exceptions.CsvFieldAssignmentException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
	private T bean;

	/**
	 * ヘッダーを生成
	 * @param bean データクラス
	 * @return 生成したヘッダー
	 */
	@Override
	public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
		super.generateHeader(bean);
		this.bean = bean;
		// データクラス(T bean)のフィールドの数
		int fieldSize = super.getFieldMap().values().size();

		// ヘッダー情報の初期化
		List<String> headerList = new LinkedList<String>();

		for (int index = 0; index < fieldSize; index++) {
			// データクラス(T bean)のフィールドを取得
			BeanField<T, Integer> beanField = super.findField(index);

			if (this.checkFieldInfoNotEmpty(beanField)) {
				this.setColumnHeader(beanField, headerList);
			} else {
				headerList.add(StringUtils.EMPTY);
			}
		}

		return headerList.toArray(new String[headerList.size()]);
	}

	/**
	 * ヘッダー情報をセットする
	 * @param beanField フィールド情報
	 * @param headerList ヘッダーリスト設定変数
	 */
	private void setColumnHeader(BeanField<?, ?> beanField, List<String> headerList) {
		if (this.isMultipleData(beanField)) {
			// 配列やリストの場合
			int maxSize = beanField.getField().getDeclaredAnnotationsByType(CsvMultipleDataMaxSize.class)[0].maxSize();

			for (int index = 1; index <= maxSize; index++) {
				StringBuilder sb = new StringBuilder();
				// header名
				sb.append(this.getHeaderName(beanField));
				// index
				sb.append(Integer.toString(index));

				headerList.add(sb.toString());
			}
		} else {
			// 単体のフィールドの場合はそのままヘッダー名リストに追加
			headerList.add(this.getHeaderName(beanField));
		}
	}

	/**
	 * フィールドががリストや配列かどうか判定
	 * @param beanField 対象のフィールド
	 * @return 判定結果
	 */
	private boolean isMultipleData(BeanField<?, ?> beanField) {

		Field field = beanField.getField();
		field.setAccessible(true);
		try {
			if (field.getType().isArray() || field.get(this.bean) instanceof java.util.List) {
				// フィールドが配列またはリストの場合
				return true;
			} else {
				return false;
			}
		} catch (IllegalArgumentException | IllegalAccessException e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * フィールド情報のNullチェック
	 * @param beanField
	 * @return 判定結果 
	 */
	private boolean checkFieldInfoNotEmpty(BeanField<?, ?> beanField) {
		return ObjectUtils.isNotEmpty(beanField)
				&& ObjectUtils.isNotEmpty(beanField.getField())
				// データクラスで宣言されたアノテーションの情報
				&& ObjectUtils.isNotEmpty(beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class));
	}

	/**
	 * フィールド情報からカラム名を取得
	 * @param beanField フィールド情報 
	 * @return 対象のカラム名
	 */
	private String getHeaderName(BeanField<?, ?> beanField) {
		return beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0].column();
	}
	
	/**
	 * データを生成
	 * @param bean データクラス
	 * @return 1行分の書き込みデータ
	 */
	@Override
	public String[] transmuteBean(T bean) throws CsvFieldAssignmentException, CsvChainedException {
		// データクラス(T bean)のフィールドの数
		int headerSize = headerIndex.findMaxIndex() + 1;

		// 書き込み情報の初期化
		List<String> writeDataList = new LinkedList<String>();

		for (int index = 0; index < headerSize; index++) {
			// データクラス(T bean)のフィールドを取得
			BeanField<T, Integer> beanField = super.findField(index);
			if (this.isMultipleData(beanField)) {
				// 配列やリストの場合
				
				// 複数データの初期化変数
				String[] multipleData = null;
				
				Field field = beanField.getField();
				if (field.getType().isArray()) {
					// フィールドが配列の場合
					multipleData = (String[]) beanField.getFieldValue(bean);
					for (String data : multipleData) {
						writeDataList.add(data);
					}
					continue;
				}

				try {
					if (field.get(bean) instanceof java.util.List) {
						// フィールドがリストの場合
						multipleData = this.fieldValueListConvertToStringArray(beanField.getFieldValue(bean));

						for (String data : multipleData) {
							writeDataList.add(data);
						}
						continue;
					}
				} catch (IllegalArgumentException | IllegalAccessException e) {
					e.printStackTrace();
				}

			} else {
				writeDataList.add(
						ObjectUtils.isNotEmpty(beanField.getFieldValue(bean)) ? beanField.getFieldValue(bean).toString()
								: StringUtils.EMPTY);
			}
		}

		return writeDataList.toArray(new String[writeDataList.size()]);
	}
	
	/**
	 * フィールドがリストの場合以下の形式でデータなので「[」「]」「 」(空白)で置換して、「,」でsplitする。
	 * データ形式 : [?, ?, ?, ?, ?, ...]
	 * 
	 * @param fieldValue フィールドのバリュー
	 * @return String[]に変換した値
	 */
	private String[] fieldValueListConvertToStringArray(Object fieldValue) {
		// 「[]」「 」消して、「,」でsplitする
		return fieldValue.toString().replaceAll("\\[", StringUtils.EMPTY).replaceAll("\\]", StringUtils.EMPTY)
				.replaceAll(" ", StringUtils.EMPTY)
				.split(",");
	}
}

```

もしかしたら上記の処理に無駄処理や足りないチェックがあるかもしれません。もし独自のカスタムをしたい場合は、以下の2つのメソッドの結果が「__フィールドに設定したヘッダー名とフィールドの値__」になるようにしてください。
```
public String[] generateHeader(T bean); // フィールドに設定したヘッダー名
public String[] transmuteBean(T bean); // フィールドの値
```

ひな形は以下のような感じでしょうか。
「__T bean__」がデータクラスなのでそのデータクラスの内容もとに独自のカスタムを行ってください。
```:CustomMappingStrategy.java ひな形
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.exceptions.CsvChainedException;
import com.opencsv.exceptions.CsvFieldAssignmentException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
	@Override
	public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        return null;
    }
    @Override
	public String[] transmuteBean(T bean) throws CsvFieldAssignmentException, CsvChainedException {
        return null;
    }
}
```

以下のメソッドも参考にしてください。
```: 使えそうなメソッド
// フィールドを取得
BeanField<T, Integer> beanField = super.findField(index);

// 型とかを知りたいときに使うもの
Field field = beanField.getField();
field.setAccessible(true);
field.getType();
// beanはgenerateHeaderやtransmuteBeanの引数
field.get(bean);

// フィールドに設定されているアノテーション情報を取得する
beanField.getField().getDeclaredAnnotationsByType(欲しいアノテーション.class)[0].アノテーションクラスに設定したメソッド()

// フィールドに設定(set～)した値を取得(Object型)
beanField.getFieldValue(bean);
```

# # 動作確認
今回作成したものを動作確認します。
以下2つも記載しときます。

Mainクラスです。
```:Main.java
package com.app;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.app.bean.CSVData1Bean;
import com.app.util.CSVUtil;

public class Main {

	public static void main(String[] args) throws Exception {

		CSVUtil<CSVData1Bean> util = new CSVUtil<CSVData1Bean>();

		String[] array = new String[3];

		for (int i = 0; i < array.length; i++) {
			array[i] = Integer.toString(i);
		}

		CSVData1Bean bean = CSVData1Bean
				.builder()
				.area("001")
				.id("001")
				.nenTukiHi("20220101")
				.listData(Arrays.asList(array))
				.arrayData(array)
				.build();
		
		// headerの内容を表示
		System.out.println(Arrays.toString(util.getHeaderField(bean, CSVData1Bean.class)));

		String path = ".\\src\\main\\resources\\csv\\result.csv";

		List<CSVData1Bean> list = new ArrayList<CSVData1Bean>() {
			{
				add(bean);
			}
		};

		util.writeCSV(list, CSVData1Bean.class, path);

	}

}

```
Utilクラスです。
```:CSVUtil.java
package com.app.util;

import java.io.File;
import java.io.FileWriter;
import java.util.List;

import com.opencsv.CSVWriter;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

public class CSVUtil<T> {
	
	/**
	 * データクラスに設定したヘッダーの取得
	 * @param bean データクラス 
	 * @param type データクラスの型
	 * @return ヘッダー名
	 * @throws CsvRequiredFieldEmptyException
	 */
	public  String[] getHeaderField(T bean, Class<? extends T> type) throws CsvRequiredFieldEmptyException {
		CustomMappingStrategy<T> mappingStrategy = new CustomMappingStrategy<>();
        mappingStrategy.setType(type);      
        return mappingStrategy.generateHeader(bean);
	}
	
	 /**
     * CSVファイルを出力する
     * @param beanList 書き込むデータリスト
     * @param bean JavaBeansのクラス
     * @param path CSVファイルのパス
     * @throws Exception
     */
    public void writeCSV(List<T> beanList, Class<T> bean, String path) throws Exception {
        File file = new File(path); 
        try (CSVWriter csvWriter = new CSVWriter(new FileWriter(file))) {
            CustomMappingStrategy<T> mappingStrategy = new CustomMappingStrategy<>();
            mappingStrategy.setType(bean);
            StatefulBeanToCsv<T> beanToCsv = new StatefulBeanToCsvBuilder<T>(csvWriter)       
            .withMappingStrategy(mappingStrategy)
            .build();
            beanToCsv.write(beanList);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }     
    }

}

```
出力結果です。
```:result.csv
"エリア","ID","年月日","listData1","listData2","listData3","arrayData1","arrayData2","arrayData3"
"001","001","20220101","0","1","2","0","1","2"
```

上記を作成したリポジトリです。

https://github.com/T-H9703EnAc/OpenCSV_Custom