AbstractCsvAnnotationBeanWriter.java

package com.github.mygreen.supercsv.io;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

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.AbstractCsvWriter;
import org.supercsv.io.CsvBeanWriter;
import org.supercsv.prefs.CsvPreference;
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.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 CsvBeanWriter
 * @version 2.3
 * @since 2.1
 * @author T.TSUCHIE
 *
 */
public abstract class AbstractCsvAnnotationBeanWriter<T> extends AbstractCsvWriter {
    
    /**
     * Beanのマッピング情報のキャッシュ。
     * ・組み立てたCellProcessorをキャッシュしておきます。
     */
    protected BeanMappingCache<T> beanMappingCache;
    
    /** temporary storage of bean values */
    protected final List<Object> beanValues = new ArrayList<>();
    
    /** temporary storage of processed columns to be written */
    protected final List<Object> processedColumns = new ArrayList<>();
    
    /** cache of methods for mapping from fields to columns */
    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 AbstractCsvAnnotationBeanWriter(final Writer writer, final CsvPreference preference) {
        super(writer, preference);
        
    }
    
    /**
     * レコードを書き込みます。
     * 
     * @param source 書き込むレコード。
     * @throws NullPointerException source is null.
     * @throws IOException レコードの出力に失敗した場合。
     * @throws SuperCsvException レコードの値に問題がある場合
     * 
     */
    public void write(final T source) throws IOException {
        
        Objects.requireNonNull(source, "the bean to write should not be null.");
        
        // update the current row/line numbers
        super.incrementRowAndLineNo();
        
        final CsvContext context = new CsvContext(getLineNumber(), getRowNumber(), 1);
        context.setRowSource(Collections.emptyList());  // 空の値を入れる
        
        final CsvBindingErrors bindingErrors = new CsvBindingErrors(beanMappingCache.getOriginal().getType());
        
        // コールバックメソッドの実行(書き込み前)
        for(CallbackMethod callback : beanMappingCache.getOriginal().getPreWriteMethods()) {
            callback.invoke(source, context, bindingErrors, beanMappingCache.getOriginal());
        }
        
        // extract the bean values
        extractBeanValues(source, beanMappingCache.getNameMapping());
        context.setRowSource(new ArrayList<Object>(beanValues));
        
        Optional<SuperCsvRowException> rowException = Optional.empty();
        try {
            executeCellProcessors(processedColumns, beanValues, beanMappingCache.getCellProcessorsForWriting(), 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) {
            // convert exception and format to message.
            errorMessages.addAll(exceptionConverter.convertAndFormat(e, beanMappingCache.getOriginal()));
            throw e;
        }
        
        // レコード、Beanの入力値検証
        if(!beanMappingCache.getOriginal().isSkipValidationOnWrite()) {
            for(CsvValidator<T> validator : validators) {
                validator.validate(source, bindingErrors, new ValidationContext<>(context, beanMappingCache.getOriginal()));
            }
        }
        
        // エラーメッセージの変換
        processErrors(bindingErrors, context, rowException);
        
        // write the list
        super.writeRow(processedColumns);
        
        // コールバックメソッドの実行(書き込み後)
        for(CallbackMethod callback : beanMappingCache.getOriginal().getPostWriteMethods()) {
            callback.invoke(source, context, bindingErrors, beanMappingCache.getOriginal());
        }
        
        // エラーメッセージの変換
        processErrors(bindingErrors, context, rowException);
        
    }
    
    /**
     * 例外発生時の処理を指定して、1レコード分を書き込みます。
     * 
     * @since 2.3
     * @param source 書き込むレコードのデータ。
     * @param errorHandler  CSVに関する例外発生時の処理の実装。
     * @return CSVの書き込み処理ステータスを返します。
     * @throws IOException
     */
    public CsvWriteStatus write(final T source, final CsvErrorHandler errorHandler) throws IOException {
        
        try {
            write(source);
            return CsvWriteStatus.SUCCESS;
            
        } catch(SuperCsvException e) {
            errorHandler.onError(e);
            return CsvWriteStatus.ERROR;
            
        }
        
    }
    
    /**
     * 行の例外情報をメッセージに変換したりします。
     * @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;
            
        }
    }
    
    /**
     * Extracts the bean values, using the supplied name mapping array.
     * 
     * @param source
     *            the bean
     * @param nameMapping
     *            the name mapping
     * @throws NullPointerException
     *             if source or nameMapping are null
     * @throws SuperCsvReflectionException
     *             if there was a reflection exception extracting the bean value
     */
    protected void extractBeanValues(final Object source, final String[] nameMapping) throws SuperCsvReflectionException {
        
        Objects.requireNonNull(nameMapping, "the nameMapping array can't be null as it's used to map from fields to columns");
        
        beanValues.clear();
        
        for( int i = 0; i < nameMapping.length; i++ ) {
            
            final String fieldName = nameMapping[i];
            
            if( fieldName == null ) {
                beanValues.add(null); // assume they always want a blank column
                
            } else {
                Method getMethod = cache.getGetMethod(source, fieldName);
                try {
                    beanValues.add(getMethod.invoke(source));
                }
                catch(final Exception e) {
                    throw new SuperCsvReflectionException(String.format("error extracting bean value for field %s",
                        fieldName), e);
                }
            }
            
        }
        
    }
    
    /**
     * 行の各カラムの値に対して、CellProcessorを適用します。
     * 
     * @param destination
     * @param source
     * @param processors
     * @param context
     */
    protected void executeCellProcessors(final List<Object> destination, final List<?> source,
            final CellProcessor[] processors, final CsvContext 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クラスを元に作成したヘッダー情報を取得する。
     * <p>ただし、列番号を省略され、定義がされていないカラムは、{@literal column[カラム番号]}の形式となります。</p>
     * @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;
    }
    
    
    
}