タツノオトシゴのブログ

主にJavaに関するものです。

Super CSV Annotationライブラリを作った

CSVのライブラリは、Javaではたくさんありますが、使い勝手のよいSuperCSVで唯一不足している機能と思われるのが、アノテーション機能でしたので作成しました。
あと、SuperCSVのプログラムを調べたら、改造する必要はなくほぼ独立して作れることが分かったので、今回のライブラリを作成しました。

公開先(Github

今まで、ずっと自宅でSubversionのサーバを立てて使っていたので、GitとGihub自体の使い方がよくわからないので、サイトがおかしなことになっているため今後修正します。

【Supser CSV 2.x用】

-- https://github.com/tatsu-no-otoshigo/super-csv-annotation/tree/master/dest

【Supser CSV 1.5用】

前提環境
  • Java1.6+
  • SuperCSV2.x

-- SuperCSV2.x自体はJava1.5+なので注意。

アノテーションに対応
  • CsvBeanWriter/CsvBeanReaderでBeanを読み込むときにAnnotationをもとにして、CellProcessorを自動的に組み立てるライブラリです。
    • 組み立てたCellProcessorを既存のCsvBeanWriter/CsvBeanReaderに渡すことができるので、完全に独立しています。
    • ただし、専用のCsvAnnotationBeanWriter/CsvAnnotationBeanReaderを使用することで、コードが短くなります。
  • 既存のCellProcessorにほぼ全て対応しています。
CellProcessorの追加
  • 既存のCellProcessorでは、あまり使用しない数値型のfloatやshortがなかったので追加しました。また、列挙型も追加しました。
  • 日付型として、java.util.Date以外ににTimestampやTimeも追加しました。。
    • その際に、日付型や数値型の書式にLocaleを指定できるようにしました。
メッセージの日本語化対応(ローカライズ
  • CellProcessorにより不正な値が読みこまれた場合、例外SuperCsvExceptionが発生するが、その例外を任意のメッセージに変換できるようにした。
  • 1行分をまとめてチェックできるValidableCsvBeanReader/ValidableCsvBeanWriterを作りました。
    • これにより、Valiationを細かくできるようにしました。
今後の予定
  • Javadocのドキュメントをキチンと書く。日本語と英語の両方対応。
  • Githubのページでjavadocなどを公開するなど。

- 古いSuperCSV1.5系に対応する。2013/07/16 対応済み。

経緯や思い

Beanにマッピングでき、かつアノテーションができるライブラリとしては、「OrangeSignal CSV」がありますが、これはなぜかBeanで読み込む/書き込むときだけ1行づつの処理ができませんでした。
そのため、大量データを処理するには、一端全てメモリ上に読み込む必要があり、メモリを多く消費してまうという問題がありました(実際に、数1000行単位で読み込まない限りメモリは圧迫しませんが…)。
しかし、Validation機能がないことを除けばOrangeSignal CSVは完璧でした。
当初、OrangeSignal CSVで、1行ずつ読み書きできるクラスを作成しようとしましたが、がっちり固められすぎで、根の深い部分から作らなければならなかったので断念しました。
また、S2Csvは仕様としては良いですが、S2Containerに依存しているため、Springを普段利用する私には使えませんでした。

今回、作成した、Super CSVアノテーション機能は、Beanにアノテーションを記述すると、Writer用/Reader用のCellProcessorを自動的に組み立てるものになります。

このライブラリを作るに当たり、下記のものを参考にしました。

途中から、BeanValidationやOValのアノテーションを使用した方がよいかともと考えましたが、既存のCellProcessorにマッチしないアノテーションがあり、結局専用のアノテーションを作ることになりました。
また、もし、BeanValidationを採用したら、文字列以外のオブジェクトをマッピングする際には常に複数の条件を設定するため、Bean自体がアノテーション地獄に陥っていました。

基本的な機能は1日で、1Kstepほどでできましたが、色々なクラスタイプに対応していたら、結局、4Ksetpになりました。
ここまで1週間かかりました。

アノテーションの使い方

基本
  • Beanには、@CsvBeanを付与します。
  • 各フィールドには、@CsvColumnを付与します。
    • プロパティ「position」は列番号を表し、0から始まります。
    • null値を取る必須でない(オプション)場合は、optional = trueを設定します。
@CsvBean(header=true)
public class SampleBean1{
    
    @CsvColumn(position = 0, optional = true)
    private int integer1 input
    
    @CsvColumn(position = 1, optional = false, unique = true)
    private Integer integer2;
    
    @CsvColumn(position = 2, optional = true, trim = true, inputDefaultValue="aa")
    public String string3;
    
    @CsvColumn(position = 3, ,outputDefaultValue="2012-10-13 00:00:00")
    public Date date4;
    
    @CsvColumn(position = 4, inputDefaultValue="RED", outputDefualtValue="BLUE")
    public Color enum5;
    
    enum Color {
       RED, BLUE, GREEN, Yellow;
    }
}
  • アノテーションの付与の仕方によって、下記のようなRead時/Write時のCellProcessorを組み立てます。
(1)build field 'integer1' CellProcessor
 #input CellProcessor
  new Optional(new ParseInt())
 
 #output CellProcessor
  new Optional()

(2)build field 'integer2' CellProcessor
 #input CellProcessor
  new NotNull(new Unique(new ParseInt()))
 
 #output CellProcessor
  new NotNull(new Unique())

(3)build field 'string3' CellProcessor
 #input CellProcessor
  new Optional(new new ConvertNullTo('aa', Trim()))
 
 #output CellProcessor
  new Optional(new Trim())
  
(4)build field 'date4' CellProcessor
 #input CellProcessor
  new ConvertNullTo(date obj('2012-10-13 00:00:00'), new NotNull(new ParseLocaleDate('yyyy-MM-dd HH:mm:ss')))
 
 #output CellProcessor
  new NotNull(ConvertNullTo(date obj('2012-10-13 00:00:00'), new FormatLocaleDate('yyyy-MM-dd HH:mm:ss')))

(5)build field 'enum5' CellProcessor
 #input CellProcessor
  new ConvertNullTo(enum obj('RED'), new NotNull(new ParseEnum()))
 
 #output CellProcessor
  new NotNull(ConvertNullTo(enum obj('BLUE')))

フォーマット指定などの細かな設定

フィールドのクラスタイプごとに、別途@CsvXXConverterアノテーションを付与することで、さらに細かくCellProcessorを指定できます。

Date型のときはフォーマット指定に使います。

  • @CsvStringConverter:String型に対する追加制約など。文字列長チェックなど。
  • @CsvNumberConverter:数値型(byte/short/int/long/float/double/ラッパークラス/BigInteger/BigDecimal)のフォーマットや大・小の制限を指定できます。
  • @CsvDateConverter:日付型( java.util.Date / java.sql.Date / java.sql.Time / java.sql.Timestamp )のフォーマットや大・小の制限を指定できます。
  • @CsvEnumConverter:列挙型のパース時の大文字・小文字を無視するなどの指定ができます。
@CsvBean(header=true)
public class SampleBean1{

    @CsvColumn(position = 0, label="数字")
    private int integer1;
    
    @CsvColumn(position = 1, optional=true)
    @CsvNumberConverter(pattern="###,###,###")
    private Integer integer2;
    
    @CsvColumn(position = 2)
    private String string1;
    
    @CsvColumn(position = 3, optional=true, inputDefaultValue="@empty")
    @CsvStringConverter(maxLength=6, contain={"1"})
    private String string2;
    
    @CsvColumn(position = 4)
    private Date date1;
    
    @CsvColumn(position = 5, optional=true)
    @CsvDateConverter(pattern="yyyy/MM/dd", min="2000/10/30")
    private Timestamp date2;
    
    @CsvColumn(position = 6, label="enum class", optional=true, inputDefaultValue="BLUE")
    @CsvEnumConveret(lenient = true)
    private Color enum1;
    
}

CSVの読み込み方

既存のCsvBeanReaderを使用する

既存のCsvBeanReaderを使用することもできます。

// CellProcessorやフィールドのマッピングをアノテーションンから作成する「CsvAnnotationBeanParser」を利用します。
CsvAnnotationBeanParser helper = new CsvAnnotationBeanParser();
CsvBeanMapping<SampleBean1> mappingBean = helper.parse(SampleBean1.class, false);

String[] nameMapping = mappingBean.getNameMapping();
CellProcessor[] cellProcessors = mappingBean.getInputCellProcessor();   // reader時専用のProcessorを取得

File inputFile = new File("src/test/data/test_error.csv");
ICsvBeanReader csvReader = new CsvBeanReader(
   new InputStreamReader(new FileInputStream(inputFile), "Windows-31j"),
       CsvPreference.STANDARD_PREFERENCE);

// write bean data.
List<SampleBean1> list = new ArrayList<SampleBean1>();
String[] headers = csvReader.getHeader(true);
while((bean1 = csvReader.read(SampleBean1.class, nameMapping, cellProcessors)) != null) {
    System.out.println(bean1);
    list.add(bean1);
}
本ライブラリのCsvAnnotationBeanReaderを使用する

既存のCsvBeanReaderよりもシンプルになります。
読み込み時などにマッピング情報を渡さなくてもよくなります。

File inputFile = new File("src/test/data/test_error.csv");
CsvAnnotationBeanReader csvReader = 
    new CsvAnnotationBeanReader(SampleBean1.class, strWriter, CsvPreference.STANDARD_PREFERENCE);

// read bean data.
List<SampleBean1> list = new ArrayList<SampleBean1>();
String[] headers = csvReader.getHeader();  // use custom method.
while((bean1 = csvReader.read()) != null) {
    System.out.println(bean1);
    list.add(bean1);
}

CSVの書き込み方

既存のCsvBeanWriterを使用する

読み込み時と同様に、CsvAnnotationBeanParserを使用します。

// create cell processor and field name mapping
CsvAnnotationBeanParser helper = new CsvAnnotationBeanParser();
CsvBeanMapping<SampleBean1> mappingBean = helper.parse(SampleBean1.class, false);

String[] nameMapping = mappingBean.getNameMapping();
CellProcessor[] cellProcessors = mappingBean.getOutputCellProcessor();  // writer時専用のProcessorを取得

StringWriter strWriter = new StringWriter();
ICsvBeanWriter csvWriter = null;
csvWriter = new CsvBeanWriter(strWriter, CsvPreference.STANDARD_PREFERENCE);

// write bean data.
List<SampleBean1> list = ...;
csvWriter.writeHeader(mappingBean.getHeader());
for(final SampleBean1 item : list) {
    csvWriter.write(item, nameMapping, cellProcessors);
    csvWriter.flush();
}
本ライブラリのCsvAnnotationBeanWriterを使用する

既存のCsvBeanWriterよりもシンプルになります。

StringWriter strWriter = new StringWriter();
CsvAnnotationBeanWriter<SampleBean1> csvWriter = 
    new CsvAnnotationBeanWriter<SampleBean1>(SampleBean1.class, 
        strWriter, CsvPreference.STANDARD_PREFERENCE);

// write bean data.
List<SampleBean1> list = ...;
csvWriter.writeHeader();  // use custom method.
for(final SampleBean1 item : list) {
    csvWriter.write(item);  // use cutom method.
    csvWriter.flush();
}

例外のメッセージの出力

基本

ValidatableCsvBeanReader/ValidatableCsvBeanWriterを使用します。
CsvBeanReader/Writerクラスは、Beanにマッピングする際に同じ列に不正な値が複数あっても、
例外をすぐにスローするため初めにチェックしたフィールドのエラーしか検知できませんでした。
ValidatableCsvBeanReader/Writerクラスは、メッセージを行ごとにため込み、
1行の処理が終わったら例外をスローするようにします。

// create cell processor and field name mapping
CsvAnnotationBeanParser helper = new CsvAnnotationBeanParser();
CsvBeanMapping<SampleBean1> mappingBean = helper.parse(SampleBean1.class, false);

String[] nameMapping = mappingBean.getNameMapping();
CellProcessor[] cellProcessors = mappingBean.getInputCellProcessor();

File inputFile = new File("src/test/data/test_error.csv");
ICsvBeanReader csvReader = new ValidatableCsvBeanReader(
   new InputStreamReader(new FileInputStream(inputFile), "Windows-31j"),
       CsvPreference.STANDARD_PREFERENCE);

// 例外をメッセージオブジェクトに変換するクラス
CsvExceptionConveter exceptionConveter = new CsvExceptionConveter();
MessageConverter messageConverter = new MessageConverter();

// read bean data.
List<SampleBean1> list = new ArrayList<SampleBean1>();
try {
    String[] headers = csvReader.getHeader(true);
    while((bean1 = csvReader.read(SampleBean1.class, nameMapping, cellProcessors)) != null) {
        System.out.println(bean1);
        list.add(bean1);
    }
} catch(SuperCsvException e) {
    // 例外をメッセージに変換する
    List<CsvMessage> csvErrors= exceptionConveter.convertCsvError(e);
    List<String> messages = messageConverter.convertMessage(csvErrors);
    for(String str : messages) {
        System.err.println(str);
    }
}
全てのメッセージを読み込む

エラーがあっても全て読み込む場合、while文の中でtry-catchします。
全て読み終わったら、CsvAnnotationBeanReader#getCsvErrors()でエラーを取得します。

File inputFile = new File("src/test/data/test.csv");
CsvAnnotationBeanReader<SampleBean1> csvReader = new CsvAnnotationBeanReader<SampleBean1>(
        SampleBean1.class,
        new InputStreamReader(new FileInputStream(inputFile), "Windows-31j"),
        CsvPreference.STANDARD_PREFERENCE);

List<SampleBean1> list = new ArrayList<SampleBean1>();
SampleBean1 bean1;
String[] headers = csvReader.getHeader();
while(true) {
    try {
        // エラーがあっても最後まで読み続ける
        bean1 = csvReader.read();
        if(bean1 == null) {
            break;
        }
        if(csvReader.hasNotError()) {
            // エラーがなければ読み込む
            list.add(bean1);
        }
    } catch(SuperCsvException e) { }
}
csvReader.close();

if(csvReader.hasError()) {
    // エラーを取得して、メッセージに変換する
    MessageConverter messageConverter = new MessageConverter();
    List<String> messages = messageConverter.convertMessage(csvReader.getCsvErrors());
    for(String str : messages) {
        System.err.println(str);
    }
    
}
メッセージのカスタマイズ

デフォルトでは、「org/supercsv/ext/SuperCsvMessages.properties」にメッセージが定義されています。
他のメッセージファイルを利用したい場合は、MessageConverterに任意のファイルを渡します。

  • 任意のメッセージファイルを渡す。
CsvExceptionConveter exceptionConveter = new CsvExceptionConveter();
MessageConverter messageConverter = new MessageConverter();
messageConverter.setMessageResolver(new ResourceBundleMessageResolver(... your resource bundle))
  • 他の実装クラスとして、SpringのMessageSourceも渡せる。
CsvExceptionConveter exceptionConveter = new CsvExceptionConveter();
MessageConverter messageConverter = new MessageConverter();
messageConverter.setMessageResolver(new SpringMessageResolver(... your resource messsage source))
  • プロパティファイルの構成は「CellProcessorのクラス名」+ ".violated" をキーとします。
message example (org/supercsv/ext/SuperCsvMessages.properties)
                • -
# common variable # ${lineNumber} = the line number of the file being read/written # ${rowNumber} = the CSV row number (CSV rows can span multiple lines) # ${columnNumber} = the CSV column number # ${value} = the invalidate value (source). csvError=(row, column)=(${lineNumber}, ${columnNumber}) : hasError csvError.noMatchColumnSize=(row)=(${lineNumber}) : no match column size. expected size=${expectedSize}, but actual size='${value}' # original CellProcessor org.supercsv.cellprocessor.ConvertNullTo.violated=(row, column)=(${lineNumber}, ${columnNumber}) : fail to convert from null object org.supercsv.cellprocessor.FmtBool.violated=(row, column)=(${lineNumber}, ${columnNumber}) : fail to format boolean from '${value}' org.supercsv.cellprocessor.FmtDate.violated=(row, column)=(${lineNumber}, ${columnNumber}) : fail to date from '${value}'