All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mygreen.supercsv.io.AbstractCsvAnnotationBeanReader Maven / Gradle / Ivy

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  マッピング対象のBeanのクラスタイプ
 *
 * @see CsvBeanReader
 * @version 2.3
 * @since 2.1
 * @author T.TSUCHIE
 *
 */
public abstract class AbstractCsvAnnotationBeanReader extends AbstractCsvReader {
    
    /**
     * Beanのマッピング情報のキャッシュ。
     * ・組み立てたCellProcessorをキャッシュしておきます。
     */
    protected BeanMappingCache beanMappingCache;
    
    /** temporary storage of processed columns to be mapped to the bean */
    protected final List 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 errorMessages = new ArrayList<>();
    
    /** validator */
    protected final List> 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(processedColumns));
            
            Optional rowException = Optional.empty();
            try {
                executeCellProcessor(processedColumns, getColumns(), beanMappingCache.getCellProcessorsForReading(), context);
                
            } catch(SuperCsvRowException e) {
                /*
                 * カラムごとのCellProcessorのエラーの場合、別なValidatorで値を検証するために、
                 * 後から判定を行うようにする。
                 */
                rowException = Optional.of(e);
                
                final List 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 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 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にマッピングしたオブジェクトです。
     * 

読み込む際には例外 {@link SuperCsvException} / {@link UncheckedIOException} が発生する可能性があります(読み込みを行った {@link Stream} メソッドからスローされます)。

*

読み込み時にスローされた {@link IOException} は、{@link UncheckedIOException} にラップされます。

* * @since 2.3 * @return 各レコードをBeanに変換した {@link Stream} を返します。 */ public Stream lines() { Iterator itr = new Iterator() { 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 rowException) { if(bindingErrors.hasErrors()) { final List 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 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 destination, final List 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 getBeanMapping() { return beanMappingCache.getOriginal(); } /** * エラーメッセージを取得します。 * @return 処理中に発生した例外をメッセージに変換した */ public List 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... validators) { this.validators.addAll(Arrays.asList(validators)); } /** * レコードの値を検証するValidatorを取得します。 * @return {@link CsvValidator}の実装クラスを設定します。 */ public List> getValidators() { return validators; } }