AbstractCsvAnnotationBeanReader.java

package com.github.mygreen.supercsv.io;

import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.exception.SuperCsvCellProcessorException;
import org.supercsv.exception.SuperCsvException;
import org.supercsv.exception.SuperCsvReflectionException;
import org.supercsv.io.AbstractCsvReader;
import org.supercsv.io.CsvBeanReader;
import org.supercsv.io.ITokenizer;
import org.supercsv.prefs.CsvPreference;
import org.supercsv.util.BeanInterfaceProxy;
import org.supercsv.util.CsvContext;
import org.supercsv.util.MethodCache;

import com.github.mygreen.supercsv.builder.BeanMapping;
import com.github.mygreen.supercsv.builder.CallbackMethod;
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.exception.SuperCsvRowException;
import com.github.mygreen.supercsv.validation.CsvBindingErrors;
import com.github.mygreen.supercsv.validation.CsvError;
import com.github.mygreen.supercsv.validation.CsvExceptionConverter;
import com.github.mygreen.supercsv.validation.CsvValidator;
import com.github.mygreen.supercsv.validation.ValidationContext;

/**
 * アノテーションを元にCSVファイルを読み込むための抽象クラス。
 * 
 * @param <T> マッピング対象のBeanのクラスタイプ
 *
 * @see CsvBeanReader
 * @version 2.3
 * @since 2.1
 * @author T.TSUCHIE
 *
 */
public abstract class AbstractCsvAnnotationBeanReader<T> extends AbstractCsvReader {
    
    /**
     * Beanのマッピング情報のキャッシュ。
     * ・組み立てたCellProcessorをキャッシュしておきます。
     */
    protected BeanMappingCache<T> beanMappingCache;
    
    /** temporary storage of processed columns to be mapped to the bean */
    protected final List<Object> processedColumns = new ArrayList<>();
    
    /** cache of methods for mapping from columns to fields */
    protected final MethodCache cache = new MethodCache();
    
    /** exception converter. */
    protected CsvExceptionConverter exceptionConverter = new CsvExceptionConverter();
    
    /** processing error messages. */
    protected final List<String> errorMessages = new ArrayList<>();
    
    /** validator */
    protected final List<CsvValidator<T>> validators = new ArrayList<>();
    
    public AbstractCsvAnnotationBeanReader(final Reader reader, final CsvPreference preference) {
        super(reader, preference);
    }
    
    public AbstractCsvAnnotationBeanReader(final ITokenizer tokenizer, final CsvPreference preference) {
        super(tokenizer, preference);
    }
    
    /**
     * 1レコード分を読み込みます。
     * 
     * @return Beanのレコード。読み込むレコードがない場合は、nullを返します。
     * 
     * @throws IOException レコードの読み込みに失敗した場合。
     * @throws SuperCsvNoMatchColumnSizeException レコードのカラムサイズに問題がある場合
     * @throws SuperCsvBindingException セルの値に問題がある場合
     * @throws SuperCsvException 設定など、その他に問題がある場合
     * 
     */
    public T read() throws IOException {
        
        if(readRow()) {
            
            final T bean = instantiateBean(beanMappingCache.getOriginal().getType());
            final CsvBindingErrors bindingErrors = new CsvBindingErrors(beanMappingCache.getOriginal().getType());
            
            final CsvContext context = new CsvContext(getLineNumber(), getRowNumber(), 1);
            context.setRowSource(new ArrayList<Object>(processedColumns));
            
            Optional<SuperCsvRowException> rowException = Optional.empty();
            try {
                executeCellProcessor(processedColumns, getColumns(), beanMappingCache.getCellProcessorsForReading(), context);
                
            } catch(SuperCsvRowException e) {
                /*
                 * カラムごとのCellProcessorのエラーの場合、別なValidatorで値を検証するために、
                 * 後から判定を行うようにする。
                 */
                rowException = Optional.of(e);
                
                final List<CsvError> errors = exceptionConverter.convert(e, beanMappingCache.getOriginal());
                bindingErrors.addAllErrors(errors);
                
            } catch(SuperCsvException e) {
                errorMessages.addAll(exceptionConverter.convertAndFormat(e, beanMappingCache.getOriginal()));
                throw e;
            }
            
            // コールバックメソッドの実行(読み込み前)
            for(CallbackMethod callback : beanMappingCache.getOriginal().getPreReadMethods()) {
                callback.invoke(bean, context, bindingErrors, beanMappingCache.getOriginal());
            }
            
            // beanへのマッピング
            populateBean(bean, beanMappingCache.getNameMapping(), bindingErrors);
            
            // Bean(レコード)の入力値検証
            for(CsvValidator<T> recordValidator : validators) {
                recordValidator.validate(bean, bindingErrors, new ValidationContext<>(context, beanMappingCache.getOriginal()));
            }
            
            // コールバックメソッドの実行(読み込み後)
            for(CallbackMethod callback : beanMappingCache.getOriginal().getPostReadMethods()) {
                callback.invoke(bean, context, bindingErrors, beanMappingCache.getOriginal());
            }
            
            // エラーメッセージの変換
            processErrors(bindingErrors, context, rowException);
            
            return bean;
            
        }
        
        return null; // EOF
        
        
    }
    
    /**
     * 成功時、例外発生時の処理を指定して、1レコード分を読み込みます。
     * 
     * @since 2.3
     * @param successHandler 読み込み成功時の処理の実装。
     * @param errorHandler CSVに関する例外発生時の処理の実装。
     * @return CSVの読み込み処理ステータスを返します。
     * @throws IOException 致命的なレコードの読み込みに失敗した場合にスローされます。
     */
    public CsvReadStatus read(final CsvSuccessHandler<T> successHandler, final CsvErrorHandler errorHandler) throws IOException {
        
        try {
            final T bean = read();
            if(bean != null) {
                successHandler.onSuccess(bean);
                return CsvReadStatus.SUCCESS;
            } else {
                return CsvReadStatus.EOF;
            }
        
        } catch(SuperCsvException e) {
            errorHandler.onError(e);
            return CsvReadStatus.ERROR;
            
        }
        
    }
    
    /**
     * {@link Stream} を返します。要素はCSVの行をBeanにマッピングしたオブジェクトです。
     * <p>読み込む際には例外 {@link SuperCsvException} / {@link UncheckedIOException} が発生する可能性があります(読み込みを行った {@link Stream} メソッドからスローされます)。</p>
     * <p>読み込み時にスローされた {@link IOException} は、{@link UncheckedIOException} にラップされます。</p>
     * 
     * @since 2.3
     * @return 各レコードをBeanに変換した {@link Stream} を返します。
     */
    public Stream<T> lines() {
        
        Iterator<T> itr = new Iterator<T>() {
            
            T nextLine = null;
            
            @Override
            public boolean hasNext() {
                if(nextLine != null) {
                    return true;
                    
                } else {
                    try {
                        nextLine = read();
                        return (nextLine != null);
                    } catch(IOException e) {
                        throw new UncheckedIOException(e);
                    }
                }
            }

            @Override
            public T next() {
                if (nextLine != null || hasNext()) {
                    T line = nextLine;
                    nextLine = null;
                    return line;
                } else {
                    throw new NoSuchElementException();
                }
            }
            
        };
        
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                itr, Spliterator.ORDERED | Spliterator.NONNULL), false);

    }
    
    /**
     * CSVのヘッダーの検証を行います。
     * 
     * @param sourceHeader オリジナルのヘッダー情報。
     * @param definedHeader アノテーションなどの定義を元にしたヘッダー情報
     * @throws SuperCsvNoMatchColumnSizeException ヘッダーのサイズ(カラム数)がBean定義と一致しない場合。
     * @throws SuperCsvNoMatchHeaderException ヘッダーの値がBean定義と一致しない場合。
     */
    protected void validateHeader(final String[] sourceHeader, final String[] definedHeader) {
        
        // check column size.
        if(sourceHeader.length != definedHeader.length) {
            final CsvContext context = new CsvContext(1, 1, 1);
            throw new SuperCsvNoMatchColumnSizeException(sourceHeader.length, definedHeader.length, context);
            
        }
        
        // check header value
        for(int i=0; i < sourceHeader.length; i++) {
            if(!sourceHeader[i].equals(definedHeader[i])) {
                final CsvContext context = new CsvContext(1, 1, i+1);
                throw new SuperCsvNoMatchHeaderException(sourceHeader, definedHeader, context);
            }
        }
        
    }
    
    /**
     * 行の例外情報をメッセージに変換したりします。
     * @param bindingErrors
     * @param context
     * @param rowException
     */
    protected void processErrors(final CsvBindingErrors bindingErrors, final CsvContext context,
            final Optional<SuperCsvRowException> rowException) {
        if(bindingErrors.hasErrors()) {
            final List<String> message = bindingErrors.getAllErrors().stream()
                    .map(error -> error.format(exceptionConverter.getMessageResolver(), exceptionConverter.getMessageInterpolator()))
                    .collect(Collectors.toList());
            errorMessages.addAll(message);
            
            final SuperCsvBindingException bindingException = new SuperCsvBindingException("has binding error.", context, bindingErrors);
            rowException.ifPresent(re -> bindingException.addAllProcessingErrors(re.getColumnErrors()));
            
            throw bindingException;
        }
    }
    
    /**
     * 指定したBeanのクラスのインスタンスを作成する。
     * 
     * @param clazz Beanのクラスタイプ。
     * @return Beanのインスタンス。
     * @throws SuperCsvReflectionException Beanのインスタンスの作成に失敗した場合。
     */
    protected T instantiateBean(final Class<T> clazz) {
        
        final T bean;
        if( clazz.isInterface() ) {
            bean = BeanInterfaceProxy.createProxy(clazz);
        } else {
            try {
                bean = clazz.newInstance();
            } catch(InstantiationException e) {
                throw new SuperCsvReflectionException(String.format(
                    "error instantiating bean, check that %s has a default no-args constructor", clazz.getName()), e);
            } catch(IllegalAccessException e) {
                throw new SuperCsvReflectionException("error instantiating bean", e);
            }
        }
        
        return bean;
        
    }
    
    /**
     * 行の各カラムの値に対して、CellProcessorを適用します。
     * @param destination
     * @param source
     * @param processors
     * @param context
     * @throws SuperCsvNoMatchColumnSizeException カラムサイズが定義と一致しない場合
     * @throws SuperCsvRowException CellProcessor内で発生した例外
     */
    protected void executeCellProcessor(final List<Object> destination, final List<String> source,
            final CellProcessor[] processors, final CsvContext context) {
        
        if(source.size() != processors.length) {
            throw new SuperCsvNoMatchColumnSizeException(source.size(), processors.length, context);
            
        }
        
        destination.clear();
        
        final SuperCsvRowException rowException = new SuperCsvRowException(
                String.format("row (%d) has errors column", context.getRowNumber()), context);
        
        for( int i = 0; i < source.size(); i++ ) {
            
            try {
                context.setColumnNumber(i + 1); // update context (columns start at 1)
                
                if( processors[i] == null ) {
                    destination.add(source.get(i)); // no processing required
                } else {
                    destination.add(processors[i].execute(source.get(i), context)); // execute the processor chain
                }
            } catch(SuperCsvCellProcessorException e) {
                rowException.addError(e);
                
                // 各カラムでエラーがあっても、後の入力値検証で処理を続けるために、仮に値を設定する。
                destination.add(source.get(i));
                
            } catch(SuperCsvException e) {
                
                throw e;
                
            }
        }
        
        if(rowException.isNotEmptyColumnErrors()) {
            throw rowException;
        }
        
    }
    
    /**
     * Beanの各フィールドに対して値を設定する。
     * @param resultBean
     * @param nameMapping
     * @param bindingErrors
     */
    protected void populateBean(final T resultBean, final String[] nameMapping, final CsvBindingErrors bindingErrors) {
        
        // map each column to its associated field on the bean
        for( int i = 0; i < nameMapping.length; i++ ) {
            final String fieldName = nameMapping[i];
            final Object fieldValue = processedColumns.get(i);
            
            // don't call a set-method in the bean if there is no name mapping for the column or no result to store
            if( fieldName == null || fieldValue == null || bindingErrors.hasFieldErrors(fieldName)) {
                continue;
            }
            
            // invoke the setter on the bean
            final Method setMethod = cache.getSetMethod(resultBean, fieldName, fieldValue.getClass());
            try {
                setMethod.invoke(resultBean, fieldValue);
                
            } catch(final Exception e) {
                throw new SuperCsvReflectionException(String.format("error invoking method %s()", setMethod.getName()), e);
            }
            
        }
        
    }
    
    /**
     * Beanクラスを元に作成したヘッダー情報を取得する。
     * @return ヘッダー一覧。
     */
    public String[] getDefinedHeader() {
        return beanMappingCache.getHeader();
    }
    
    /**
     * Beanのマッピング情報を取得します。
     * @return Beanのマッピング情報
     */
    public BeanMapping<T> getBeanMapping() {
        return beanMappingCache.getOriginal();
    }
    
    /**
     * エラーメッセージを取得します。
     * @return 処理中に発生した例外をメッセージに変換した
     */
    public List<String> getErrorMessages() {
        return errorMessages;
    }
    
    /**
     * 処理中に発生した例外をメッセージに変換するクラスを取得します。
     * @return 
     */
    public CsvExceptionConverter getExceptionConverter() {
        return exceptionConverter;
    }
    
    /**
     * 処理中に発生した例外をメッセージに変換するクラスを設定します。
     * @param exceptionConverter 独自にカスタマイズした値を設定します。
     */
    public void setExceptionConverter(CsvExceptionConverter exceptionConverter) {
        this.exceptionConverter = exceptionConverter;
    }
    
    /**
     * レコードの値を検証するValidatorを追加します。
     * @param validators {@link CsvValidator}の実装クラスを設定します。
     */
    @SuppressWarnings("unchecked")
    public void addValidator(CsvValidator<T>... validators) {
        this.validators.addAll(Arrays.asList(validators));
    }
    
    /**
     * レコードの値を検証するValidatorを取得します。
     * @return {@link CsvValidator}の実装クラスを設定します。
     */
    public List<CsvValidator<T>> getValidators() {
        return validators;
    }
    
}