package com.github.mygreen.supercsv.builder; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.TreeSet; import java.util.stream.Collectors; import org.supercsv.exception.SuperCsvException; import com.github.mygreen.supercsv.annotation.CsvColumn; import com.github.mygreen.supercsv.annotation.CsvPartial; import com.github.mygreen.supercsv.exception.SuperCsvInvalidAnnotationException; import com.github.mygreen.supercsv.io.LazyCsvAnnotationBeanReader; import com.github.mygreen.supercsv.io.LazyCsvAnnotationBeanWriter; import com.github.mygreen.supercsv.localization.MessageBuilder; /** * {@link BeanMapping}を組み立てる時のヘルパクラス。 * * @version 2.2 * @since 2.1 * @author T.TSUCHIE * */ public class BeanMappingFactoryHelper { /** * カラム番号が重複しているかチェックする。また、番号が1以上かもチェックする。 * @param beanType Beanタイプ * @param list カラム情報の一覧 * @return チェック済みの番号 * @throws SuperCsvInvalidAnnotationException {@link CsvColumn}の定義が間違っている場合 */ public static TreeSet<Integer> validateDuplicatedColumnNumber(final Class<?> beanType, final List<ColumnMapping> list) { final TreeSet<Integer> checkedNumber = new TreeSet<>(); final TreeSet<Integer> duplicatedNumbers = new TreeSet<>(); for(ColumnMapping columnMapping : list) { if(checkedNumber.contains(columnMapping.getNumber())) { duplicatedNumbers.add(columnMapping.getNumber()); } checkedNumber.add(columnMapping.getNumber()); } if(!duplicatedNumbers.isEmpty()) { // 重複している 属性 numberが存在する場合 throw new SuperCsvInvalidAnnotationException(MessageBuilder.create("anno.attr.duplicated") .var("property", beanType.getName()) .varWithAnno("anno", CsvColumn.class) .var("attrName", "number") .var("attrValues", duplicatedNumbers) .format()); } // カラム番号が1以上かチェックする final int minColumnNumber = checkedNumber.first(); if(minColumnNumber <= 0) { throw new SuperCsvInvalidAnnotationException(MessageBuilder.create("anno.attr.min") .var("property", beanType.getName()) .varWithAnno("anno", CsvColumn.class) .var("attrName", "number") .var("attrValue", minColumnNumber) .var("min", 1) .format()); } return checkedNumber; } /** * 欠けているカラム番号がある場合、その番号を持つダミーのカラムを追加する。 * @param beanType Beanタイプ * @param list カラム情報の一覧 * @param partialAnno Beanに設定されているアノテーション{@link CsvPartial}の情報。 * @param suppliedHeaders 提供されたヘッダー。提供されてない場合は、長さ0の配列。 * @return */ public static TreeSet<Integer> supplyLackedNumberMappingColumn(final Class<?> beanType, final List<ColumnMapping> list, final Optional<CsvPartial> partialAnno, final String[] suppliedHeaders) { final TreeSet<Integer> checkedNumber = list.stream() .filter(col -> col.isDeterminedNumber()) .map(col -> col.getNumber()) .collect(Collectors.toCollection(TreeSet::new)); // 定義されている列番号の最大値 final int maxColumnNumber = checkedNumber.last(); // Beanに定義されていない欠けているカラム番号の取得 final TreeSet<Integer> lackedNumbers = new TreeSet<Integer>(); for(int i=1; i <= maxColumnNumber; i++) { if(!checkedNumber.contains(i)) { lackedNumbers.add(i); } } // 定義されているカラム番号より、大きなカラム番号を持つカラム情報の補足 if(partialAnno.isPresent()) { final int partialColumnSize = partialAnno.get().columnSize(); if(maxColumnNumber > partialColumnSize) { throw new SuperCsvInvalidAnnotationException(partialAnno.get(), MessageBuilder.create("anno.CsvPartial.columSizeMin") .var("property", beanType.getName()) .var("columnSize", partialColumnSize) .var("maxColumnNumber", maxColumnNumber) .format()); } if(maxColumnNumber < partialColumnSize) { for(int i= maxColumnNumber+1; i <= partialColumnSize; i++) { lackedNumbers.add(i); } } } // 不足分のカラムがある場合は、部分的な読み書き用カラムとして追加する if(lackedNumbers.size() > 0) { for(int number : lackedNumbers) { list.add(createPartialColumnMapping(number, partialAnno, getSuppliedHeaders(suppliedHeaders, number))); } list.sort(null); } return lackedNumbers; } /** * 提供されたヘッダーから該当するカラム番号のヘッダーを取得する。 * @param suppliedHeaders 提供されたヘッダー。提供されてない場合は、長さ0の配列。 * @param columnNumber カラム番号。1から始まる。 * @return 該当するカラムのヘッダー。見つからない場合は空を返す。 */ private static Optional<String> getSuppliedHeaders(final String[] suppliedHeaders, final int columnNumber) { final int length = suppliedHeaders.length; if(length == 0) { return Optional.empty(); } if(columnNumber < length) { return Optional.ofNullable(suppliedHeaders[columnNumber-1]); } return Optional.empty(); } /** * 部分的なカラムの場合の作成 * @param columnNumber 列番号 * @param partialAnno アノテーション {@literal @CsvPartial}のインスタンス * @param suppliedHeader 補完対象のヘッダーの値 * @return 部分的なカラム情報。 */ private static ColumnMapping createPartialColumnMapping(int columnNumber, final Optional<CsvPartial> partialAnno, final Optional<String> suppliedHeader) { final ColumnMapping columnMapping = new ColumnMapping(); columnMapping.setNumber(columnNumber); columnMapping.setPartialized(true); String label = String.format("column%d", columnNumber); if(suppliedHeader.isPresent()) { label = suppliedHeader.get(); } if(partialAnno.isPresent()) { for(CsvPartial.Header header : partialAnno.get().headers()) { if(header.number() == columnNumber) { label = header.label(); break; } } } columnMapping.setLabel(label); return columnMapping; } /** * カラム番号が決定していないカラムをチェックする。 * <p>{@link LazyCsvAnnotationBeanReader}/{@link LazyCsvAnnotationBeanWriter}において、 * CSVファイルや初期化時のヘッダーが不正により、該当するラベルがヘッダーに見つからないときをチェックする。 * </p> * * @since 2.2 * @param beanType Beanタイプ * @param list カラム情報の一覧 * @param headers ヘッダー * @throws SuperCsvException カラム番号が決定していないとき */ public static void validateNonDeterminedColumnNumber(final Class<?> beanType, final List<ColumnMapping> list, String[] headers) { final List<String> nonDeterminedLabels = list.stream() .filter(col -> !col.isDeterminedNumber()) .map(col -> col.getLabel()) .collect(Collectors.toList()); if(!nonDeterminedLabels.isEmpty()) { throw new SuperCsvException(MessageBuilder.create("lazy.noDeteminedColumns") .var("property", beanType.getName()) .var("labels", nonDeterminedLabels) .var("headers", headers) .format()); } } }