BeanMappingFactoryHelper.java

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());
            
        }
        
    }
    
}