XlsLoader.java

package com.gh.mygreen.xlsmapper;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gh.mygreen.xlsmapper.annotation.XlsFieldProcessor;
import com.gh.mygreen.xlsmapper.annotation.XlsListener;
import com.gh.mygreen.xlsmapper.annotation.XlsPostLoad;
import com.gh.mygreen.xlsmapper.annotation.XlsPreLoad;
import com.gh.mygreen.xlsmapper.annotation.XlsSheet;
import com.gh.mygreen.xlsmapper.fieldaccessor.FieldAccessor;
import com.gh.mygreen.xlsmapper.fieldaccessor.FieldAccessorFactory;
import com.gh.mygreen.xlsmapper.fieldaccessor.FieldAccessorProxy;
import com.gh.mygreen.xlsmapper.fieldaccessor.FieldAccessorProxyComparator;
import com.gh.mygreen.xlsmapper.fieldprocessor.FieldProcessor;
import com.gh.mygreen.xlsmapper.fieldprocessor.ProcessCase;
import com.gh.mygreen.xlsmapper.localization.MessageBuilder;
import com.gh.mygreen.xlsmapper.util.ArgUtils;
import com.gh.mygreen.xlsmapper.util.ClassUtils;
import com.gh.mygreen.xlsmapper.util.Utils;
import com.gh.mygreen.xlsmapper.validation.MultipleSheetBindingErrors;
import com.gh.mygreen.xlsmapper.validation.SheetBindingErrors;
import com.gh.mygreen.xlsmapper.xml.AnnotationReader;


/**
 * ExcelのシートをJavaBeanにマッピングするクラス。
 *
 * @version 2.0
 * @author T.TSUCHIE
 *
 */
public class XlsLoader {

    private static final Logger logger = LoggerFactory.getLogger(XlsLoader.class);

    private Configuration configuration;

    /**
     * 独自のシステム情報を設定するコンストラクタ
     * @param configuration システム情報
     */
    public XlsLoader(final Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * デフォルトのコンストラクタ
     */
    public XlsLoader() {
        this(new Configuration());
    }

    /**
     * Excelファイルの1シートを読み込み、任意のクラスにマッピングする。
     *
     * @param <P> シートをマッピングするクラスタイプ
     * @param xlsIn 読み込みもとのExcelファイルのストリーム。
     * @param clazz マッピング先のクラスタイプ。
     * @return シートをマッピングしたオブジェクト。
     *         {@link Configuration#isIgnoreSheetNotFound()}の値がtrueで、シートが見つからない場合、nullを返します。
     * @throws IllegalArgumentException {@literal xlsIn == null or clazz == null}
     * @throws XlsMapperException Excelファイルのマッピングに失敗した場合
     * @throws IOException ファイルの読み込みに失敗した場合
     *
     */
    public <P> P load(final InputStream xlsIn, final Class<P> clazz)  throws XlsMapperException, IOException {

        ArgUtils.notNull(xlsIn, "xlsIn");
        ArgUtils.notNull(clazz, "clazz");

        return loadDetail(xlsIn, clazz).getTarget();
    }

    /**
     * Excelファイルの1シートを読み込み、任意のクラスにマッピングする。
     *
     * @param <P> シートをマッピングするクラスタイプ
     * @param xlsIn 読み込み元のExcelファイルのストリーム。
     * @param clazz マッピング先のクラスタイプ。
     * @return マッピングの詳細情報。
     *         {@link Configuration#isIgnoreSheetNotFound()}の値がtrueで、シートが見つからない場合、nullを返します。
     *
     * @throws IllegalArgumentException {@literal xlsIn == null or clazz == null}
     * @throws XlsMapperException Excelファイルのマッピングに失敗した場合
     * @throws IOException ファイルの読み込みに失敗した場合
     */
    public <P> SheetBindingErrors<P> loadDetail(final InputStream xlsIn, final Class<P> clazz)
            throws XlsMapperException, IOException {

        ArgUtils.notNull(xlsIn, "xlsIn");
        ArgUtils.notNull(clazz, "clazz");

        final AnnotationReader annoReader = new AnnotationReader(configuration.getAnnotationMapping().orElse(null));

        final XlsSheet sheetAnno = annoReader.getAnnotation(clazz, XlsSheet.class);
        if(sheetAnno == null) {
            throw new AnnotationInvalidException(sheetAnno, MessageBuilder.create("anno.notFound")
                    .varWithClass("property", clazz)
                    .varWithAnno("anno", XlsSheet.class)
                    .format());
        }

        Workbook book = null;
        try {
            book = WorkbookFactory.create(xlsIn);

        } finally {
            if(book != null) {
                book.close();
            }
        }

        try {
            final Sheet[] xlsSheet = configuration.getSheetFinder().findForLoading(book, sheetAnno, annoReader, clazz);
            return loadSheet(xlsSheet[0], clazz, annoReader);

        } catch(SheetNotFoundException e) {
            if(configuration.isIgnoreSheetNotFound()){
                logger.warn(MessageBuilder.create("log.skipNotFoundSheet").format(), e);
                return null;

            } else {
                throw e;
            }
        }
    }

    /**
     * Excelファイルの同じ形式の複数シートを読み込み、任意のクラスにマップする。
     * <p>{@link XlsSheet#regex()}により、複数のシートが同じ形式で、同じクラスにマッピングすする際に使用します。</p>
     *
     * @param <P> シートをマッピングするクラスタイプ
     * @param xlsIn 読み込み元のExcelファイルのストリーム。
     * @param clazz マッピング先のクラスタイプ。
     * @return マッピングした複数のシート。
     *         {@link Configuration#isIgnoreSheetNotFound()}の値がtrueで、シートが見つからない場合、マッピング結果には含まれません。
     * @throws IllegalArgumentException {@literal xlsIn == null or clazz == null}
     * @throws XlsMapperException マッピングに失敗した場合
     * @throws IOException ファイルの読み込みに失敗した場合
     */
    @SuppressWarnings("unchecked")
    public <P> P[] loadMultiple(final InputStream xlsIn, final Class<P> clazz) throws XlsMapperException, IOException {

        return loadMultipleDetail(xlsIn, clazz).getAll().stream()
                .map(s -> s.getTarget())
                .toArray(n -> (P[])Array.newInstance(clazz, n));
    }

    /**
     * Excelファイルの同じ形式の複数シートを読み込み、任意のクラスにマップする。
     * <p>{@link XlsSheet#regex()}により、複数のシートが同じ形式で、同じクラスにマッピングすする際に使用します。</p>
     *
     * @param <P> シートをマッピングするクラスタイプ
     * @param xlsIn 読み込み元のExcelファイルのストリーム。
     * @param clazz マッピング先のクラスタイプ。
     * @return 複数のシートのマッピング結果。
     *         {@link Configuration#isIgnoreSheetNotFound()}の値がtrueで、シートが見つからない場合、マッピング結果には含まれません。
     * @throws IllegalArgumentException {@literal xlsIn == null or clazz == null}
     * @throws XlsMapperException マッピングに失敗した場合
     * @throws IOException ファイルの読み込みに失敗した場合
     */
    public <P> MultipleSheetBindingErrors<P> loadMultipleDetail(final InputStream xlsIn, final Class<P> clazz)
            throws XlsMapperException, IOException {

        ArgUtils.notNull(xlsIn, "xlsIn");
        ArgUtils.notNull(clazz, "clazz");

        final AnnotationReader annoReader = new AnnotationReader(configuration.getAnnotationMapping().orElse(null));

        final XlsSheet sheetAnno = annoReader.getAnnotation(clazz, XlsSheet.class);
        if(sheetAnno == null) {
            throw new AnnotationInvalidException(sheetAnno, MessageBuilder.create("anno.notFound")
                    .varWithClass("property", clazz)
                    .varWithAnno("anno", XlsSheet.class)
                    .format());
        }

        final MultipleSheetBindingErrors<P> multipleResult = new MultipleSheetBindingErrors<>();

        Workbook book = null;
        try {
            book = WorkbookFactory.create(xlsIn);

        } finally {
            if(book != null) {
                book.close();
            }
        }

        if(sheetAnno.number() == -1 && sheetAnno.name().isEmpty() && sheetAnno.regex().isEmpty()) {
            // 読み込むシートの条件が指定されていない場合、全て読み込む
            int sheetNum = book.getNumberOfSheets();
            for(int i=0; i < sheetNum; i++) {
                final Sheet sheet = book.getSheetAt(i);

                multipleResult.addBindingErrors(loadSheet(sheet, clazz, annoReader));

            }

        } else {
            // 読み込むシートの条件が指定されている場合
            try {
                final Sheet[] xlsSheet = configuration.getSheetFinder().findForLoading(book, sheetAnno, annoReader, clazz);
                for(Sheet sheet : xlsSheet) {
                    multipleResult.addBindingErrors(loadSheet(sheet, clazz, annoReader));

                }

            } catch(SheetNotFoundException e) {
                if(configuration.isIgnoreSheetNotFound()){
                    logger.warn(MessageBuilder.create("log.skipNotFoundSheet").format(), e);
                } else {
                    throw e;
                }
            }

        }

        return multipleResult;
    }

    /**
     * Excelファイルの異なる形式の複数シートを読み込み、任意のクラスにマップする。
     * <p>複数のシートの形式を一度に読み込む際に使用します。</p>
     *
     * @param xlsIn 読み込み元のExcelファイルのストリーム。
     * @param classes マッピング先のクラスタイプの配列。
     * @return マッピングした複数のシート。
     *         {@link Configuration#isIgnoreSheetNotFound()}の値がtrueで、シートが見つからない場合、マッピング結果には含まれません。
     * @throws IllegalArgumentException {@literal xlsIn == null or classes == null}
     * @throws IllegalArgumentException {@literal calsses.length == 0}
     * @throws XlsMapperException マッピングに失敗した場合
     * @throws IOException ファイルの読み込みに失敗した場合
     */
    public Object[] loadMultiple(final InputStream xlsIn, final Class<?>[] classes)
            throws XlsMapperException, IOException {
        return loadMultipleDetail(xlsIn, classes).getAll().stream()
                .map(s -> s.getTarget())
                .toArray();
    }

    /**
     * Excelファイルの異なる形式の複数シートを読み込み、任意のクラスにマップする。
     * <p>複数のシートの形式を一度に読み込む際に使用します。</p>
     *
     * @param xlsIn 読み込み元のExcelファイルのストリーム。
     * @param classes マッピング先のクラスタイプの配列。
     * @return マッピングした複数のシートの結果。
     *         {@link Configuration#isIgnoreSheetNotFound()}の値がtrueで、シートが見つからない場合、マッピング結果には含まれません。
     * @throws IllegalArgumentException {@literal xlsIn == null or classes == null}
     * @throws IllegalArgumentException {@literal calsses.length == 0}
     * @throws IOException ファイルの読み込みに失敗した場合
     * @throws XlsMapperException マッピングに失敗した場合
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public MultipleSheetBindingErrors<Object> loadMultipleDetail(final InputStream xlsIn, final Class<?>[] classes)
            throws XlsMapperException, IOException {

        ArgUtils.notNull(xlsIn, "xlsIn");
        ArgUtils.notEmpty(classes, "classes");

        final AnnotationReader annoReader = new AnnotationReader(configuration.getAnnotationMapping().orElse(null));

        final MultipleSheetBindingErrors<Object> multipleStore = new MultipleSheetBindingErrors<>();

        Workbook book = null;
        try {
            book = WorkbookFactory.create(xlsIn);

        } finally {
            if(book != null) {
                book.close();
            }
        }

        for(Class<?> clazz : classes) {
            final XlsSheet sheetAnno = clazz.getAnnotation(XlsSheet.class);
            if(sheetAnno == null) {
                throw new AnnotationInvalidException(sheetAnno, MessageBuilder.create("anno.notFound")
                        .varWithClass("property", clazz)
                        .varWithAnno("anno", XlsSheet.class)
                        .format());
            }

            try {
                final Sheet[] xlsSheet = configuration.getSheetFinder().findForLoading(book, sheetAnno, annoReader, clazz);
                for(Sheet sheet : xlsSheet) {
                    multipleStore.addBindingErrors(loadSheet(sheet, (Class)clazz, annoReader));

                }

            } catch(SheetNotFoundException ex){
                if(!configuration.isIgnoreSheetNotFound()){
                    logger.warn(MessageBuilder.create("log.skipNotFoundSheet").format(), ex);
                    throw ex;
                }
            }

        }

        return multipleStore;
    }

    /**
     * シートを読み込み、任意のクラスにマッピングする。
     * @param sheet シート情報
     * @param clazz マッピング先のクラスタイプ。
     * @param annoReader
     * @return シートのマッピング情報
     * @throws XlsMapperException
     *
     */
    private <P> SheetBindingErrors<P> loadSheet(final Sheet sheet, final Class<P> clazz, final AnnotationReader annoReader)
            throws XlsMapperException {

        // 値の読み込み対象のJavaBeanオブジェクトの作成
        final P beanObj = configuration.createBean(clazz);

        final SheetBindingErrors<P> errors =  configuration.getBindingErrorsFactory().create(beanObj);
        errors.setSheetName(sheet.getSheetName());
        errors.setSheetIndex(sheet.getWorkbook().getSheetIndex(sheet));

        final LoadingWorkObject work = new LoadingWorkObject();
        work.setAnnoReader(annoReader);
        work.setErrors(errors);

        // セルのキャッシュ情報の初期化
        configuration.getCellFormatter().init(configuration.isCacheCellValueOnLoad());

        final FieldAccessorFactory adpterFactory = new FieldAccessorFactory(annoReader);

        // リスナークラスの@PreLoad用メソッドの実行
        final XlsListener listenerAnno = annoReader.getAnnotation(beanObj.getClass(), XlsListener.class);
        if(listenerAnno != null) {
            for(Class<?> listenerClass : listenerAnno.value()) {
                final Object listenerObj = configuration.createBean(listenerClass);

                for(Method method : listenerObj.getClass().getMethods()) {
                    if(annoReader.hasAnnotation(method, XlsPreLoad.class)) {
                        Utils.invokeNeedProcessMethod(listenerObj, method, beanObj, sheet, configuration, work.getErrors(), ProcessCase.Load);
                    }
                }
            }

        }

        // @PreLoad用のメソッドの実行
        for(Method method : clazz.getMethods()) {

            if(annoReader.hasAnnotation(method, XlsPreLoad.class)) {
                Utils.invokeNeedProcessMethod(beanObj, method, beanObj, sheet, configuration, work.getErrors(), ProcessCase.Load);
            }
        }

        final List<FieldAccessorProxy> accessorProxies = new ArrayList<>();

        // public メソッドの処理
        for(Method method : clazz.getMethods()) {
            method.setAccessible(true);
            for(Annotation anno : annoReader.getAnnotations(method)) {
                final XlsFieldProcessor annoFieldProcessor = anno.annotationType().getAnnotation(XlsFieldProcessor.class);
                if(ClassUtils.isAccessorMethod(method) && annoFieldProcessor != null) {
                    // 登録済みのFieldProcessorの取得
                    FieldProcessor<?> processor = configuration.getFieldProcessorRegistry().getProcessor(anno.annotationType());

                    // アノテーションに指定されているFieldProcessorの場合
                    if(processor == null && annoFieldProcessor.value().length > 0) {
                        processor = configuration.createBean(annoFieldProcessor.value()[0]);

                    }

                    if(processor != null) {
                        final FieldAccessor accessor = adpterFactory.create(method);
                        final FieldAccessorProxy accessorProxy = new FieldAccessorProxy(anno, processor, accessor);
                        if(!accessorProxies.contains(accessorProxy)) {
                            accessorProxies.add(accessorProxy);
                        }

                    } else {
                        // FieldProcessorが見つからない場合
                        throw new AnnotationInvalidException(anno, MessageBuilder.create("anno.XlsFieldProcessor.notResolve")
                                .varWithAnno("anno", anno.annotationType())
                                .format());
                    }

                }

                if(anno instanceof XlsPostLoad) {
                    work.addNeedPostProcess(new NeedProcess(beanObj, beanObj, method));
                }
            }

        }

        // フィールドの処理
        for(Field field : clazz.getDeclaredFields()) {

            field.setAccessible(true);
            for(Annotation anno : annoReader.getAnnotations(field)) {

                final XlsFieldProcessor annoFieldProcessor = anno.annotationType().getAnnotation(XlsFieldProcessor.class);
                if(annoFieldProcessor != null) {

                    // 登録済みのFieldProcessorの取得
                    FieldProcessor<?> processor = configuration.getFieldProcessorRegistry().getProcessor(anno.annotationType());

                    // アノテーションに指定されているFieldProcessorの場合
                    if(processor == null && annoFieldProcessor.value().length > 0) {
                        processor = configuration.createBean(annoFieldProcessor.value()[0]);

                    }

                    if(processor != null) {
                        final FieldAccessor accessor = adpterFactory.create(field);
                        final FieldAccessorProxy accessorProxy = new FieldAccessorProxy(anno, processor, accessor);
                        if(!accessorProxies.contains(accessorProxy)) {
                            accessorProxies.add(accessorProxy);
                        }

                    } else {
                        // FieldProcessorが見つからない場合
                        throw new AnnotationInvalidException(anno, MessageBuilder.create("anno.XlsFieldProcessor.notResolve")
                                .varWithAnno("anno", anno.annotationType())
                                .format());
                    }

                }


            }
        }

        // 順番を並び替えて保存処理を実行する
        Collections.sort(accessorProxies, new FieldAccessorProxyComparator());
        for(FieldAccessorProxy accessorProxy : accessorProxies) {
            accessorProxy.loadProcess(sheet, beanObj, configuration, work);
        }

        // リスナークラスの@PostLoadの取得
        if(listenerAnno != null) {
            for(Class<?> listenerClass : listenerAnno.value()) {
                Object listenerObj = configuration.createBean(listenerClass);
                for(Method method : listenerObj.getClass().getMethods()) {
                    if(annoReader.hasAnnotation(method, XlsPostLoad.class)) {
                        work.addNeedPostProcess(new NeedProcess(beanObj, listenerObj, method));
                    }
                }
            }

        }

        //@PostLoadが付与されているメソッドの実行
        for(NeedProcess need : work.getNeedPostProcesses()) {
            Utils.invokeNeedProcessMethod(need.getProcess(), need.getMethod(), need.getTarget(), sheet, configuration, work.getErrors(), ProcessCase.Load);
        }

        // セルのキャッシュ情報の初期化
        configuration.getCellFormatter().init(configuration.isCacheCellValueOnLoad());

        return errors;
    }

    /**
     * システム情報を取得します。
     * @return 現在のシステム情報
     */
    public Configuration getConfiguration() {
        return configuration;
    }

    /**
     * システム情報を設定します。
     * @param configuration システム情報
     */
    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

}