OpenCSVの使用方法について

# 初めに
本記事は、JavaのOpenCSVの使用方法について記載しています。Gradleを使用したJavaの開発環境があれば問題ないと思います。わからない方は以下の参考にしてください。

https://qiita.com/T-H9703EnAc/items/002538a2b040fc188705

# OpenCSVを導入
以下からライブラリを追加してください。

http://opencsv.sourceforge.net/dependency-info.html

以下のようにbuild.gradleを記述してください。
```:build.gradle
plugins {
    id 'application'
    // 外部jarを読み込めるようにする
    id 'com.github.johnrengelman.shadow' version '7.1.2'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'junit:junit:4.13.2'

    implementation 'com.google.guava:guava:30.1.1-jre'
    // 追加 opencsv
    implementation 'com.opencsv:opencsv:5.6'
    // 追加 lombok
    implementation 'org.projectlombok:lombok:1.18.24'
}

application {
    mainClass = 'app.App'
}

// jar追加
jar {
    manifest {
        attributes 'Main-Class': 'app.App'
    }
}
```
# OpenCSVの使用方法
OpenCSVの使用方法について記載します。本記事では、単純なCSVファイルの出力方法とCSVファイルの読み込み方法、JavaBeansを使用したCSVファイルの出力方法とCSVファイルの読み込みについて記述します。

## 単純なCSVファイルの出力方法
List<String[]>形式のデータをCSV出力する方法です。配列が1行のデータ(カンマ区切りの数)を指し、リストが行数を指します。
```: Pattern1WriteCSVUtil.java
package app.pattern1.util;

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

import com.opencsv.CSVWriter;

public class Pattern1WriteCSVUtil {
    /**
     * CSVファイルを出力する
     * @param path CSVファイルのパス
     * @param dataList 書き込むデータリスト
     * @throws Exception
     */
    public static void writeCSV(String path, List<String[]> dataList) throws Exception{
        File file = new File(path);
        try (CSVWriter csvWriter = new CSVWriter(new FileWriter(file))) {
            csvWriter.writeAll(dataList);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }
    }
}
```
## 単純なCSVファイルの読み込み方法
CSVデータをList<String[]>形式で読み込む方法です。配列が1行のデータ(カンマ区切りの数)を指し、リストが行数を指します。
```:Pattern1ReadCSVUtil.java
package app.pattern1.util;

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

import com.opencsv.CSVReader;

public class Pattern1ReadCSVUtil {
    /**
     * CSVファイルを読み込む
     * @param path CSVファイルのパス
     * @return 読み込んだCSVデータ
     * @throws Exception
     */
    public static List<String[]> readCSV(String path) throws Exception{
        File file = new File(path);
        try (CSVReader csvReader = new CSVReader(new FileReader(file))) {            
            return csvReader.readAll();
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }
    }   
}
```
## JavaBeansを使用したCSVファイルの出力方法
JavaBeansを使用したCSVファイルの出力方法です。例えば、「ID、名前、年齢、所属」の情報をもつ「 __UserInfoBean.java__ 」というJavaBeansのクラスがあるとします。この時、 [単純なCSVファイルの読み込み方法](#単純なcsvファイルの読み込み方法)の方法だと毎回、データをString型に変換しないといけないので面倒ですね。「 __UserInfoBean.java__ 」のまま出力させられたら便利ですね。今回はその方法についてです。

### JavaBeansの修正
以下のような「 __UserInfoBean.java__ 」を想定します。
```: UserInfoBean.java(変更前)
package app.pattern2.bean;

import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvBindByPosition;

import org.apache.commons.lang3.builder.ToStringBuilder;

import lombok.Data;

@Data
public class UserInfoBean {
    private int id;

    private String name;

    private int age;

    private String belong;

    public UserInfoBean(int id, String name, int age, String belong){
        this.id = id;
        this.name = name;
        this.age = age;
        this.belong = belong;
    }
    
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}
```
これを以下のように変更してください。

```:UserInfoBean.java(変更後)
package app.pattern2.bean;

import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvBindByPosition;

import org.apache.commons.lang3.builder.ToStringBuilder;

import lombok.Data;

@Data
public class UserInfoBean {
    @CsvBindByPosition(position = 0)
    @CsvBindByName(column = "ID", required = true)
    private int id;

    @CsvBindByPosition(position = 1)
    @CsvBindByName(column = "名前", required = true)
    private String name;

    @CsvBindByPosition(position = 2)
    @CsvBindByName(column = "年齢", required = true)
    private int age;

    @CsvBindByPosition(position = 3)
    @CsvBindByName(column = "所属", required = true)
    private String belong;

    public UserInfoBean(int id, String name, int age, String belong){
        this.id = id;
        this.name = name;
        this.age = age;
        this.belong = belong;
    }
    
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}
```
2つの<span>@<span>(アノテーション)を追加しました。それぞれ、「__<span>@<span>CsvBindByPosition__」はCSVファイルを出力する際のヘッダーの順番を指定します。次に、「__<span>@<span>CsvBindByName__」はヘッダーの名前と必須チェックの有無を指定しています。

### JavaBeans形式でCSV出力する方法
以下の公式サイトを以下のように実装しました。

http://opencsv.sourceforge.net/#writing_from_a_list_of_beans

```: 公式サイトのBeanを読み込む方法
@Test 
public void test() throws Exception{
    // csv読み込み(Bean)
    String path = "./csv/result.csv";
    UserInfoBean beans = new UserInfoBean(1,"Tanaka",20,"チームB");
    Writer writer = new FileWriter(path);
    StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer).build();
    beanToCsv.write(beans);
    writer.close();       
}  
```
すると以下のような結果になってしいます。ヘッダー名が設定されていませんね。
上記の実装だと2つのアノテーションのうちどちらか片方しか、機能しません。
```:# CSVの結果
"1","Tanaka","20","チームB"
```
2つのアノテーションの両方が機能するようにします。

まずは、「__ColumnPositionMappingStrategy__」を継承した「__CustomMappingStrategy__」を作成します。パッケージ以外はコピペでOKです。よくわからないかと思いますが、「__extractHeaderName__」や「__columnHeaderName__」とあるのでなんとなくJavaBeansに設定した「__<span>@<span>CsvBindByName__」から良い感じにヘッダーの名前を取得しようとしていると思ってください。

```:CustomMappingStrategy.java
package app.pattern2.util;

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

import org.apache.commons.lang3.StringUtils;

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T>{
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        final int numColumns = getFieldMap().values().size();
        super.generateHeader(bean);

        String[] header = new String[numColumns];

        BeanField beanField;
        for (int i = 0; i < numColumns; i++) {
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        }
        return header;
    }

    private String extractHeaderName(final BeanField beanField) {
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(
                CsvBindByName.class).length == 0) {
            return StringUtils.EMPTY;
        }

        final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    }
}
```
任意のJavaBeansを読み込めるCSV出力機能の実装
```:Pattern2WriteCSVUtil.java
package app.pattern2.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;

public class Pattern2WriteCSVUtil<T>{
    /**
     * 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)
            .withSeparator(CSVWriter.DEFAULT_SEPARATOR)        
            .withMappingStrategy(mappingStrategy)
            .build();
            beanToCsv.write(beanList);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }     
    }
}
```
使い方は以下のように行います。
```:
Pattern2WriteCSVUtil<使用したいJavaBeans> 任意の変数名 = new Pattern2WriteCSVUtil<使用したいJavaBeans>();
任意の変数名.writeCSV();
```
実際に以下のように呼び出してみます。
```
@Test 
public void test() throws Exception{
   String path = "./csv/result.csv";
   List<UserInfoBean> beanList = new ArrayList<>();
   UserInfoBean bean = new UserInfoBean(1,"Tanaka",20,"チームB");
   beanList.add(bean);
   Pattern2WriteCSVUtil<UserInfoBean> util  = new Pattern2WriteCSVUtil<UserInfoBean>();  
   util.writeCSV(beanList, UserInfoBean.class, path); 
}
```
上記の出力結果です。header名も設定されていますね。
```:# CSVの結果
"ID","名前","年齢","所属"
"1","Tanaka","20","チームB"
```
CSVの出力はこれで終わりです。

### JavaBeans形式でCSV読み込みする方法
CSV読み込みは書き込みほど面倒ではありません。ほぼ公式サイトと同じ実装です。

http://opencsv.sourceforge.net/#quick_start

```:Pattern2ReadCSVUtil.java
package app.pattern2.util;

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

import com.opencsv.CSVReader;
import com.opencsv.bean.CsvToBeanBuilder;

public class Pattern2ReadCSVUtil<T> {
    public  List<T> readCSV(Class<T> bean, String path) throws Exception{
        File file = new File(path);
        try (CSVReader csvReader = new CSVReader(new FileReader(file))) {
            CsvToBeanBuilder<T> builder = new CsvToBeanBuilder<T>(csvReader);
            builder.withType(bean);                      
            return builder.build().parse();
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }
    }
}
```

ここで注意して欲しいことがあります。「__UserInfoBean__」は __id__ や __age__ が __int型__ になっています。しかし、CSVファイルから読み込む際はString型の扱いになるので「__UserInfoBean__」で読み込むとエラーになります。
よって以下のように新たに「__UserInfoBean2__」を作成します。
```:UserInfoBean2.java
package app.pattern2.bean;

import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvBindByPosition;

import org.apache.commons.lang3.builder.ToStringBuilder;

import lombok.Data;

@Data
public class UserInfoBean2 {
    @CsvBindByPosition(position = 0)
    @CsvBindByName(column = "ID", required = true)
    private String id;

    @CsvBindByPosition(position = 1)
    @CsvBindByName(column = "名前", required = true)
    private String name;

    @CsvBindByPosition(position = 2)
    @CsvBindByName(column = "年齢", required = true)
    private String age;

    @CsvBindByPosition(position = 3)
    @CsvBindByName(column = "所属", required = true)
    private String belong;

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}
```
[先ほど](#javabeans形式でcsv出力する方法)出力したCSVを読み込んでみましょう。
```:
@Test 
public void test6() throws Exception{
   String path = "./csv/result.csv";           
   Pattern2ReadCSVUtil<UserInfoBean2> csvUtil = new Pattern2ReadCSVUtil<UserInfoBean2>();
   List<UserInfoBean2> dataList = csvUtil.readCSV(UserInfoBean2.class, path);
   for(UserInfoBean2 info : dataList){
       System.out.println(info);
   }      
}
```
上記の実行結果です。ちゃんと変数と値が紐づいてますね。
```:# 実行結果
app.pattern2.bean.UserInfoBean2@6d7b4f4c[age=年齢,belong=所属,id=ID,name=名前]
app.pattern2.bean.UserInfoBean2@eec5a4a[age=20,belong=チームB,id=1,name=Tanaka]
```
# 最後に
ここまでご覧いただきありがとうございます。以下は投稿者が作成した今回のOpenCSVの記事で使用した4パターンの読み書きの機能を実装したJavaプロジェクトです。記事では省いている部分もありますのでご参考までに・・・。


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

# 参考

https://qiita.com/sunnycloudy764/items/c117e769863dc96edc59

https://tigertaizo.hatenablog.com/entry/2019/12/18/210000