LazyCsvAnnotationBeanReader.java
package com.github.mygreen.supercsv.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.supercsv.exception.SuperCsvException;
import org.supercsv.io.ITokenizer;
import org.supercsv.prefs.CsvPreference;
import com.github.mygreen.supercsv.annotation.CsvPartial;
import com.github.mygreen.supercsv.builder.BeanMapping;
import com.github.mygreen.supercsv.builder.BeanMappingFactoryHelper;
import com.github.mygreen.supercsv.builder.ColumnMapping;
import com.github.mygreen.supercsv.builder.HeaderMapper;
import com.github.mygreen.supercsv.builder.LazyBeanMappingFactory;
import com.github.mygreen.supercsv.exception.SuperCsvBindingException;
import com.github.mygreen.supercsv.exception.SuperCsvNoMatchColumnSizeException;
import com.github.mygreen.supercsv.exception.SuperCsvNoMatchHeaderException;
import com.github.mygreen.supercsv.localization.MessageBuilder;
/**
* カラムの定義が曖昧なアノテーションを元にCSVファイルを読み込むためのクラス。
* <p>カラム番号が指定されていないBean定義を元にマッピングします。
* <br>カラム番号の決定は、ヘッダー行を取得して、その情報を元に読み込み時に決定します。
* </p>
*
* @param <T> マッピング対象のBeanのクラスタイプ
*
* @version 2.2
* @since 2.1
* @author T.TSUCHIE
*
*/
public class LazyCsvAnnotationBeanReader<T> extends AbstractCsvAnnotationBeanReader<T> {
/**
* Beanのマッピング情報。
* ・初期化は済んでいない場合があるため、キャッシュとは別に管理する。
*/
private final BeanMapping<T> beanMapping;
/**
* ヘッダー情報を元に初期化済みかどうか
*/
private boolean initialized = false;
/**
* Beanのクラスタイプを指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
* <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
*
* @param beanType Beanのクラスタイプ。
* @param reader the Reader。
* @param preference the CSV preferences.
* @param groups グループ情報。適用するアノテーションを切り替える際に指定します。
* @throws NullPointerException {@literal if beanType or reader or preferences are null.}
*/
public LazyCsvAnnotationBeanReader(final Class<T> beanType, final Reader reader, final CsvPreference preference,
final Class<?>... groups) {
super(reader, preference);
Objects.requireNonNull(beanType, "beanType should not be null.");
LazyBeanMappingFactory factory = new LazyBeanMappingFactory();
this.beanMapping = factory.create(beanType, groups);
this.validators.addAll(beanMapping.getValidators());
}
/**
* Beanのマッピング情報を指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
* <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
* <p>Beanのマッピング情報を独自にカスタマイズして、{@link LazyBeanMappingFactory}から作成する場合に利用します。</p>
*
* @param beanMapping Beanのマッピング情報。
* @param reader the Reader。
* @param preference the CSV preferences.
* @throws NullPointerException {@literal if beanMapping or reader or preferences are null.}
*/
public LazyCsvAnnotationBeanReader(final BeanMapping<T> beanMapping, final Reader reader, final CsvPreference preference,
final Class<?>... groups) {
super(reader, preference);
Objects.requireNonNull(beanMapping, "beanMapping should not be null.");
this.beanMapping = beanMapping;
this.validators.addAll(beanMapping.getValidators());
}
/**
* Beanのクラスタイプを指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
* <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
*
* @param beanType Beanのクラスタイプ。
* @param tokenizer the tokenizer.
* @param preference the CSV preferences.
* @param groups グループ情報。適用するアノテーションを切り替える際に指定します。
* @throws NullPointerException {@literal if beanType or tokenizer or preferences are null.}
*/
public LazyCsvAnnotationBeanReader(final Class<T> beanType, final ITokenizer tokenizer, final CsvPreference preference,
final Class<?>... groups) {
super(tokenizer, preference);
Objects.requireNonNull(beanType, "beanType should not be null.");
LazyBeanMappingFactory factory = new LazyBeanMappingFactory();
this.beanMapping = factory.create(beanType, groups);
this.validators.addAll(beanMapping.getValidators());
}
/**
* Beanのマッピング情報を指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
* <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
* <p>Beanのマッピング情報を独自にカスタマイズして、{@link LazyBeanMappingFactory}から作成する場合に利用します。</p>
*
* @param beanMapping Beanのマッピング情報。
* @param tokenizer the tokenizer.
* @param preference the CSV preferences.
* @param groups グループ情報。適用するアノテーションを切り替える際に指定します。
* @throws NullPointerException {@literal if beanMapping or tokenizer or preferences are null.}
*/
public LazyCsvAnnotationBeanReader(final BeanMapping<T> beanMapping, final ITokenizer tokenizer, final CsvPreference preference,
final Class<?>... groups) {
super(tokenizer, preference);
Objects.requireNonNull(beanMapping, "beanMapping should not be null.");
this.beanMapping = beanMapping;
this.validators.addAll(beanMapping.getValidators());
}
/**
* 1行目のレコードをヘッダー情報として読み込んで、カラム情報を初期化を行います。
*
* @return 読み込んだヘッダー情報
* @throws SuperCsvNoMatchColumnSizeException ヘッダーのサイズ(カラム数)がBean定義と一致しない場合。
* @throws SuperCsvNoMatchHeaderException ヘッダーの値がBean定義と一致しない場合。
* @throws SuperCsvException 引数firstLineCheck=trueのとき、このメソッドが1行目以外の読み込み時に呼ばれた場合。
* @throws IOException ファイルの読み込みに失敗した場合。
*/
public String[] init() throws IOException {
// ヘッダーを元に、カラム情報の番号を補完する
final String[] headers = getHeader(true);
init(headers);
return headers;
}
/**
* ヘッダー情報を指定して、カラム情報の初期化を行います。
* <p>ヘッダーの位置を元にカラムの番号を決定します。</p>
*
* @param headers CSVのヘッダー情報。実際のCSVファイルの内容と一致する必要があります。
* @throws SuperCsvNoMatchColumnSizeException ヘッダーのサイズ(カラム数)がBean定義と一致しない場合。
* @throws SuperCsvNoMatchHeaderException ヘッダーの値がBean定義と一致しない場合。
* @throws SuperCsvException 引数firstLineCheck=trueのとき、このメソッドが1行目以外の読み込み時に呼ばれた場合。
*/
public void init(final String... headers) {
setupMappingColumns(headers);
this.beanMappingCache = BeanMappingCache.create(beanMapping);
if(beanMappingCache.getOriginal().isValidateHeader()) {
try {
validateHeader(headers, beanMapping.getHeader());
} catch(SuperCsvNoMatchColumnSizeException | SuperCsvNoMatchHeaderException e) {
// convert exception and format to message.
errorMessages.addAll(exceptionConverter.convertAndFormat(e, beanMappingCache.getOriginal()));
throw e;
}
}
// 初期化完了
this.initialized = true;
}
/**
* 初期化が完了していないときに呼ばれたときにスローする例外のインスタンスを作成します。
*/
private IllegalStateException newNotInitialzedException() {
return new IllegalStateException(MessageBuilder.create("noinit.onLazyRead").format());
}
/**
* レコードを全て読み込みます。
* <p>ヘッダー行も自動的に処理されます。</p>
* <p>レコード処理中に例外が発生した場合、その時点で処理を終了します。</p>
*
* @return 読み込んだレコード情報。
*
* @throws IOException レコードの読み込みに失敗した場合。
* @throws SuperCsvNoMatchColumnSizeException レコードのカラムサイズに問題がある場合
* @throws SuperCsvBindingException セルの値に問題がある場合
* @throws SuperCsvException 設定など、その他に問題がある場合
* @throws IllegalStateException ヘッダー行を持たないときに、{@link #init(String...)}で初期化が済んでいない場合。
*/
public List<T> readAll() throws IOException {
return readAll(false);
}
/**
* レコードを全て読み込みます。
* <p>ヘッダー行も自動的に処理されます。</p>
*
* @param continueOnError レコードの処理中に、
* 例外{@link SuperCsvNoMatchColumnSizeException}、{@link SuperCsvNoMatchColumnSizeException}、{@link SuperCsvBindingException}
* が発生しても続行するかどう指定します。
* trueの場合、例外が発生しても、次の処理を行います。
* @return 読み込んだレコード情報。
*
* @throws IOException レコードの読み込みに失敗した場合。
* @throws SuperCsvNoMatchColumnSizeException レコードのカラムサイズに問題がある場合
* @throws SuperCsvBindingException セルの値に問題がある場合
* @throws SuperCsvException 設定など、その他に問題がある場合
* @throws IllegalStateException ヘッダー行を持たないときに、{@link #init(String...)}で初期化が済んでいない場合。
*
*/
public List<T> readAll(final boolean continueOnError) throws IOException {
if(!initialized) {
if(beanMapping.isHeader()) {
// ヘッダーがファイルに存在する場合、1行目を読み込んで初期化を行う。
try {
init();
} catch(SuperCsvNoMatchColumnSizeException | SuperCsvNoMatchHeaderException e) {
if(!continueOnError) {
throw e;
}
}
} else {
// ヘッダーがファイルに存在しない場合、独自にinit(header1, header2)メソッドを呼んで初期化する必要がある。
throw newNotInitialzedException();
}
}
final List<T> list = new ArrayList<>();
while(true) {
try {
final T record = read();
if(record == null) {
break;
}
list.add(record);
} catch(SuperCsvNoMatchColumnSizeException | SuperCsvBindingException e) {
if(!continueOnError) {
throw e;
}
}
}
return list;
}
/**
* {@inheritDoc}
* @throws IllegalStateException ヘッダーが読み込まれておらず、マッピング情報の初期か完了していない場合。
*/
@Override
public T read() throws IOException {
// ヘッダーが読み込まれておらず、初期化が終わっていない場合
if(!initialized) {
throw newNotInitialzedException();
}
return super.read();
}
/**
* 読み込んだヘッダーを元に、マッピング情報を補完する。
* <p>カラムの位置である番号を確定する。</p>
* <p>存在しないカラムがある場合は、部分的な読み込みとして、ダミーのカラム情報を作成する。</p>
* @param headers
* @param columns
* @param SuperCsvInvalidAnnotationException
*/
private void setupMappingColumns(final String[] headers) {
final List<ColumnMapping> columnMappingList = beanMapping.getColumns();
final HeaderMapper headerMapper = beanMapping.getHeaderMapper();
// 一致するラベルがあれば、カラムの番号を補完する
final int headerSize = headers.length;
for(int i=0; i < headerSize;i ++) {
final String header = headers[i];
/*
* 番号が決まっておらず、ラベルが一致するカラム情報を抽出する。
* ※既に番号が決まっているが、ラベルが一致しないのものは、後からチェックする。
*/
List<ColumnMapping> undeterminedColumnList = columnMappingList.stream()
.filter(col -> col.getNumber() <= 0)
.filter(col -> headerMapper.toMap(col, beanMapping.getConfiguration(), beanMapping.getGroups()).equals(header))
.collect(Collectors.toList());
final int columnNumber = i+1;
undeterminedColumnList.forEach(col -> col.setNumber(columnNumber));
}
// カラムの番号順に並び変える
columnMappingList.sort(null);
// 決定していないカラム番号のチェック
BeanMappingFactoryHelper.validateNonDeterminedColumnNumber(beanMapping.getType(), columnMappingList, headers);
// 重複しているカラム番号のチェック
BeanMappingFactoryHelper.validateDuplicatedColumnNumber(beanMapping.getType(), columnMappingList);
// 不足しているカラム番号の補完
final Optional<CsvPartial> partialAnno = Optional.ofNullable(beanMapping.getType().getAnnotation(CsvPartial.class));
BeanMappingFactoryHelper.supplyLackedNumberMappingColumn(beanMapping.getType(), columnMappingList, partialAnno, headers);
beanMapping.setColumns(columnMappingList);
}
/**
* {@inheritDoc}
* @throws IllegalStateException {@link #init()} メソッドによる初期化が完了していない場合
*/
@Override
public String[] getDefinedHeader() {
if(!initialized) {
throw newNotInitialzedException();
}
return super.getDefinedHeader();
}
/**
* {@inheritDoc}
* @throws IllegalStateException {@link #init()} メソッドによる初期化が完了していない場合
*/
@Override
public BeanMapping<T> getBeanMapping() {
if(!initialized) {
throw newNotInitialzedException();
}
return super.getBeanMapping();
}
}