SheetBindingErrors.java

package com.gh.mygreen.xlsmapper.validation;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.stream.Collectors;

import com.gh.mygreen.xlsmapper.util.PropertyTypeNavigator;
import com.gh.mygreen.xlsmapper.util.PropertyValueNavigator;
import com.gh.mygreen.xlsmapper.util.Utils;
import com.gh.mygreen.xlsmapper.validation.fieldvalidation.FieldFormatter;
import com.gh.mygreen.xlsmapper.validation.fieldvalidation.FieldFormatterRegistry;
import com.github.mygreen.cellformatter.lang.ArgUtils;


/**
 * 1シート分のエラー情報を管理するクラス。
 * 
 * @param <P> シートにマッピングするクラスタイプ
 * @version 2.0
 * @author T.TSUCHIE
 *
 */
public class SheetBindingErrors<P> {
    
    /** パスの区切り文字 */
    public static final String PATH_SEPARATOR = ".";
    
    /**
     * 検証対象のオブジェクト。
     * ・ルートオブジェクト
     */
    private final P target;
    
    /**
     * オブジェクト名
     */
    private final String objectName;
    
    /**
     * シート名
     */
    private String sheetName;
    
    /**
     * シートのインデックス
     */
    private int sheetIndex = -1;
    
    /**
     * 現在のパス。
     * キャッシュ用。
     */
    private String currentPath;
    
    /** 
     * 検証対象のオブジェクトの現在のパス
     */
    private Stack<String> nestedPathStack = new Stack<>();
    
    /**
     * エラーオブジェクト
     */
    private final List<ObjectError> errors = new ArrayList<>();
    
    /**
     * フィールドの値のフォーマッタの管理クラス
     */
    private FieldFormatterRegistry fieldFormatterRegistry = new FieldFormatterRegistry();
    
    /** エラーコードの候補を生成するクラス */
    private MessageCodeGenerator messageCodeGenerator = new MessageCodeGenerator();
    
    /**
     * プロパティ式から、値を取得する。
     * ・private/protectedなどのフィールドにもアクセス可能にする。
     */
    private final PropertyValueNavigator propertyValueNavigator = new PropertyValueNavigator();
    {
        propertyValueNavigator.setAllowPrivate(true);
        propertyValueNavigator.setIgnoreNull(true);
        propertyValueNavigator.setIgnoreNotFoundKey(true);
        propertyValueNavigator.setCacheWithPath(true);
    }
    
    /**
     * プロパティ式から、クラスタイプを取得する。
     * ・private/protectedなどのフィールドにもアクセス可能にする。
     */
    private final PropertyTypeNavigator propertyTypeNavigator = new PropertyTypeNavigator();
    {
        propertyTypeNavigator.setAllowPrivate(true);
        propertyTypeNavigator.setIgnoreNotResolveType(true);
        propertyTypeNavigator.setCacheWithPath(true);
    }
    
    /**
     * オブジェクト名を指定しするコンストラクタ。
     * <p>エラーメッセージを組み立てる際に、パスのルートとなる。
     * @param target 検証対象のオブジェクト
     * @param objectName オブジェクト名
     */
    public SheetBindingErrors(final P target, final String objectName) {
        
        this.target = target;
        this.objectName = objectName;
        
        this.fieldFormatterRegistry.init();
    }
    
    /**
     * クラス名をオブジェクト名とするコンストラクタ。
     * <p>オブジェクト名として、{@link Class#getCanonicalName()}を設定します。</p>
     * @param target 検証対象のオブジェクト
     * @throws IllegalArgumentException {@link target == null.}
     */
    public SheetBindingErrors(final P target) {
        this(target, target.getClass().getCanonicalName());
    }
    
    /**
     * 検証対象のオブジェクトを取得する。
     * @return
     */
    public P getTarget() {
        return target;
    }
    
    /**
     * 現在のオブジェクト名称を取得する
     * @return コンストラクタで設定したオブジェクト名称。
     */
    public String getObjectName() {
        return objectName;
    }
    
    /**
     * 現在のシート名を取得する。
     * @return シート名称
     */
    public String getSheetName() {
        return sheetName;
    }
    
    /**
     * 現在のシート名を設定します。
     * @param sheetName シートの名称
     */
    public void setSheetName(final String sheetName) {
        this.sheetName = sheetName;
    }
    
    /**
     * シート番号を取得する
     * @return 0から始まる。ただし、シートが設定されていない状態の時は、-1を返す。
     */
    public int getSheetIndex() {
        return sheetIndex;
    }
    
    /**
     * シート番号を設定する
     * @param sheetIndex シート番号(0から始まる)
     */
    public void setSheetIndex(int sheetIndex) {
        this.sheetIndex = sheetIndex;
    }
    
    /**
     * 指定したパスのフィールドのクラスタイプを取得する。
     * @since 2.0
     * @param field フィールド名
     * @return クラスタイプ。ただし、リストなどGenericsのタイプが指定されていない場合、クラスタイプもnullとなる。
     */
    public Class<?> getFieldType(final String field) {
        
        final String fieldPath = buildFieldPath(field);
        Class<?> type = propertyTypeNavigator.getPropertyType(target.getClass(), fieldPath);
        if(type != null) {
            return type;
        }
        
        return getActualFieldType(fieldPath);
    }
    
    /**
     * 指定したパスのフィールドのクラスタイプを取得する。
     * <p>インスタンスを元に取得するため、サブクラスの可能性がある。</p>
     * @since 2.0
     * @param field フィールド名
     * @return クラスタイプ。ただし、オブジェクトの値がnullの場合は、クラスタイプもnullとなる。
     */
    public Class<?> getActualFieldType(final String field) {
        
        final Object fieldValue = getFieldValue(field);
        return fieldValue == null ? null : fieldValue.getClass();
        
    }
    
    /**
     * 指定したパスのフィールドの値を取得する。
     * <p>フィールドエラーにエラーが存在するときは、エラーオブジェクトから値を取得し、存在しない場合は、実際の値を取得する。</p>
     * @param field フィールド名
     * @return フィールドの値。
     */
    public Object getFieldValue(final String field) {
        
        final FieldError error = getFirstFieldError(field).orElse(null);
        if(error != null && !error.isConversionFailure()) {
            return error.getRejectedValue();
        } else {
            return getFieldActualValue(field);
        }
        
    }
    
    /**
     * 指定したパスのフィールドの値を取得する。
     * @since 2.0
     * @param field フィールド名
     * @return フィールドの値。
     */
    public Object getFieldActualValue(final String field) {
        final String fieldPath = buildFieldPath(field);
        return propertyValueNavigator.getProperty(target, fieldPath);
    }
    
    /**
     * 現在のパス上のプロパティの値を取得します。
     * <p>{@link #getTarget()}で取得できるルートオブジェクトに対して、{@link #getCurrentPath()}のパスで示された値を取得します。</p>
     * @return 現在のパス上の値。
     */
    public Object getValue() {
        final String currentPath = getCurrentPath();
        if(Utils.isEmpty(currentPath)) {
            return target;
        } else {
            return propertyValueNavigator.getProperty(target, currentPath);
        }
    }
    
    /**
     * 指定したパスで現在のパスを初期化します。
     * <p>nullまたは空文字を与えると、トップに移動します。
     * @param nestedPath ネストするパス
     */
    public void setNestedPath(final String nestedPath) {
        final String canonicalPath = normalizePath(nestedPath);
        this.nestedPathStack.clear();
        if(canonicalPath.isEmpty()) {
            this.currentPath = buildPath();
        } else {
            pushNestedPath(canonicalPath);
        }
    }
    
    /**
     * 現在のパスをルートに移動します。
     */
    public void setRootPath() {
        setNestedPath(null);
    }
    
    /**
     * パスを正規化する。
     * <ol>
     *  <li>トリムする。</li>
     *  <li>値がnullの場合は、空文字を返す。</li>
     *  <li>最後に'.'がついている場合、除去する。</li>
     * </ol>
     * @param subPath
     * @return
     */
    private String normalizePath(final String subPath) {
        if(subPath == null) {
            return "";
        }
        
        String value = subPath.trim();
        if(value.isEmpty()) {
            return value;
        }
        
        if(value.startsWith(PATH_SEPARATOR)) {
            value = value.substring(1);
        }
        
        if(value.endsWith(PATH_SEPARATOR)) {
            value = value.substring(0, value.length()-1);
        }
        
        return value;
        
    }
    
    /**
     * パスを1つ下位に移動します。
     * @param subPath ネストするパス
     * @throws IllegalArgumentException subPath is empty.
     */
    public void pushNestedPath(final String subPath) {
        final String canonicalPath = normalizePath(subPath);
        ArgUtils.notEmpty(canonicalPath, "canonicalPath");
        
        this.nestedPathStack.push(canonicalPath);
        this.currentPath = buildPath();
    }
    
    /**
     * 配列やリストなどのインデックス付きのパスを1つ下位に移動します。
     * @param subPath ネストするパス
     * @param index インデックス番号(0から始まります。)
     * @throws IllegalArgumentException {@literal subPath is empty or index < 0}
     */
    public void pushNestedPath(final String subPath, final int index) {
        final String canonicalPath = normalizePath(subPath);
        ArgUtils.notEmpty(subPath, "subPath");
        ArgUtils.notMin(index, -1, "index");
        
        pushNestedPath(String.format("%s[%d]", canonicalPath, index));
    }
    
    /**
     * マップなどのキー付きのパスを1つ下位に移動します。
     * @param subPath ネストするパス
     * @param key マップのキー
     * @throws IllegalArgumentException {@literal subPath is empty or key is empty}
     */
    public void pushNestedPath(final String subPath, final String key) {
        final String canonicalPath = normalizePath(subPath);
        ArgUtils.notEmpty(subPath, "subPath");
        ArgUtils.notEmpty(key, "key");
        
        pushNestedPath(String.format("%s[%s]", canonicalPath, key));
    }
    
    /**
     * パスを1つ上位に移動します。
     * @return 現在のパスを返しまます。
     * @throws IllegalStateException {@literal パスがこれ以上移動できない場合}
     */
    public String popNestedPath() {
        
        if(nestedPathStack.isEmpty()) {
            throw new IllegalStateException("Cannot pop nested path: no nested path on stack");
        }
        
        final String subPath = nestedPathStack.pop();
        this.currentPath = buildPath();
        return subPath;
    }
    
    /**
     * 現在パスのスタックに積まれているパスを結合し、1つに組み立てる。
     * <p>ルートの時は空文字を返します。</p>
     * @return 結合したパス
     */
    private String buildPath() {
        return Utils.join(nestedPathStack, PATH_SEPARATOR);
    }
    
    /**
     * 現在のパスを取得します。
     * <p>ルートの時は空文字を返します。</p>
     * @return 現在のパス
     */
    public String getCurrentPath() {
        return currentPath;
    }
    
    /**
     * 現在のパスに引数で指定したフィールド名を追加した値を返す。
     * <p>現在のパスが空の場合は、フィールド名を返す。</p>
     * @param fieldName フィールド名
     * @return フィールド名を追加したパス
     */
    public String buildFieldPath(final String fieldName) {
        if(Utils.isEmpty(getCurrentPath())) {
            return fieldName;
        } else {
            return Utils.join(new String[]{getCurrentPath(), fieldName}, PATH_SEPARATOR);
        }
    }
    
    /**
     * 全てのエラーをクリアする。
     * @since 0.5
     */
    public void clearAllErrors() {
        this.errors.clear();
    }
    
    /**
     * エラー情報を追加する
     * @param error エラー情報
     * @throws IllegalArgumentException {@literal error == null.}
     */
    public void addError(final ObjectError error) {
        ArgUtils.notNull(error, "error");
        this.errors.add(error);
    }
    
    /**
     * エラー情報を全て追加する。
     * @param errors エラー情報
     * @throws IllegalArgumentException {@literal errors == null.}
     */
    public void addAllErrors(final Collection<ObjectError> errors) {
        ArgUtils.notNull(errors, "errors");
        this.errors.addAll(errors);
    }
    
    /**
     * 全てのエラー情報を取得する
     * @return 全てのエラー情報
     */
    public List<ObjectError> getAllErrors() {
        return new ArrayList<>(errors);
    }
    
    /**
     * エラーがあるか確かめる。
     * @return true:エラーがある。
     */
    public boolean hasErrors() {
        return errors.size() > 0;
    }
    
    /**
     * グローバルエラーを取得する
     * @return エラーがない場合は空のリストを返す
     */
    public List<ObjectError> getGlobalErrors() {
        return errors.stream()
                .filter(e -> !(e instanceof FieldError))
                .collect(Collectors.toList());
    }
    
    /**
     * 先頭のグローバルエラーを取得する。
     * @return 存在しない場合は、空を返す。
     */
    public Optional<ObjectError> getFirstGlobalError() {
        return errors.stream()
                .filter(e -> !(e instanceof FieldError))
                .findFirst();
        
    }
    
    /**
     * グローバルエラーがあるか確かめる。
     * @return true:グローバルエラーがある。
     */
    public boolean hasGlobalErrors() {
        return getFirstGlobalError().isPresent();
    }
    
    /**
     * グローバルエラーの件数を取得する
     * @return エラーの件数
     */
    public int getGlobalErrorCount() {
        return getGlobalErrors().size();
    }
    
    /**
     * フィールドエラーを取得する
     * @return エラーがない場合は空のリストを返す
     */
    public List<FieldError> getFieldErrors() {
        return errors.stream()
                .filter(e -> e instanceof FieldError)
                .map(e -> (FieldError)e)
                .collect(Collectors.toList());
        
    }
    
    /**
     * 先頭のフィールドエラーを取得する
     * @return エラーがない場合は空を返す
     */
    public Optional<FieldError> getFirstFieldError() {
        return errors.stream()
                .filter(e -> e instanceof FieldError)
                .map(e -> (FieldError)e)
                .findFirst();
        
    }
    
    /**
     * フィールドエラーが存在するか確かめる。
     * @return true:フィールドエラーを持つ。
     */
    public boolean hasFieldErrors() {
        return getFirstFieldError().isPresent();
    }
    
    /**
     * フィールドエラーの件数を取得する。
     * @return フィールドエラーの件数
     */
    public int getFieldErrorCount() {
        return getFieldErrors().size();
    }
    
    /**
     * パスを指定してフィールドエラーを取得する。
     * <p>検索する際には、引数「path」に現在のパス({@link #getCurrentPath()})を付与して処理します。</p>
     * @param path 最後に'*'を付けるとワイルドカードが指定可能。
     * @return エラーがない場合は空のリストを返す
     */
    public List<FieldError> getFieldErrors(final String path) {
        final String fullPath = buildFieldPath(path);
        
        return getFieldErrors().stream()
                .filter(e -> isMatchingFieldError(fullPath, e))
                .collect(Collectors.toList());
        
    }
    
    /**
     * パスを指定して先頭のフィールドエラーを取得する。
     * <p>検索する際には、引数「path」に現在のパス({@link #getCurrentPath()})を付与して処理します。</p>
     * @param path 最後に'*'を付けるとワイルドカードが指定可能。
     * @return エラーがない場合は空を返す
     */
    public Optional<FieldError> getFirstFieldError(final String path) {
        final String fullPath = buildFieldPath(path);
        return getFieldErrors().stream()
                .filter(e -> isMatchingFieldError(fullPath, e))
                .findFirst();
        
    }
    
    /**
     * 指定したパスのフィィールドエラーが存在するか確かめる。
     * @param path 最後に'*'を付けるとワイルドカードが指定可能。
     * @return true:エラーがある場合。
     */
    public boolean hasFieldErrors(final String path) {
        return getFirstFieldError(path).isPresent();
    }
    
    /**
     * 指定したパスのフィィールドエラーの件数を取得する。
     * @param path 最後に'*'を付けるとワイルドカードが指定可能。
     * @return
     */
    public int getFieldErrorCount(final String path) {
        return getFieldErrors(path).size();
    }
    
    /**
     * 指定したパスがフィールドエラーのパスと一致するかチェックするかどうか。
     * @param path パス
     * @param fieldError フィールドエラー
     * @return true: 一致する場合。
     */
    private boolean isMatchingFieldError(final String path, final FieldError fieldError) {
        
        if (fieldError.getField().equals(path)) {
            return true;
        }
        
        if(path.endsWith("*")) {
            String subPath = path.substring(0, path.length()-1);
            return fieldError.getField().startsWith(subPath);
        }
        
        return false;
    }
    
    /**
     * グローバルエラーのビルダーを作成します。
     * @param errorCode エラーコード
     * @return {@link ObjectError}のインスタンスを組み立てるビルダクラス。
     */
    public InternalObjectErrorBuilder createGlobalError(final String errorCode) {
        return createGlobalError(new String[]{errorCode});
    }
    
    /**
     * グローバルエラーのビルダーを作成します。
     * @param errorCodes エラーコード。先頭の要素が優先されます。
     * @return {@link ObjectError}のインスタンスを組み立てるビルダクラス。
     */
    public InternalObjectErrorBuilder createGlobalError(final String[] errorCodes) {
        
        String[] codes = new String[0];
        for(String errorCode : errorCodes) {
            codes = Utils.concat(codes, generateMessageCodes(errorCode));
        }
        
        return new InternalObjectErrorBuilder(this, getObjectName(), codes)
                .sheetName(getSheetName());
    }
    
    /**
     * フィールドエラーのビルダーを作成します。
     * @param field フィールドパス。
     * @param errorCode エラーコード
     * @return {@link FieldError}のインスタンスを組み立てるビルダクラス。
     */
    public InternalFieldErrorBuilder createFieldError(final String field, final String errorCode) {
        
        return createFieldError(field, new String[]{errorCode});
        
    }
    
    /**
     * フィールドエラーのビルダーを作成します。
     * @param field フィールドパス。
     * @param errorCodes エラーコード。先頭の要素が優先されます。
     * @return {@link FieldError}のインスタンスを組み立てるビルダクラス。
     */
    public InternalFieldErrorBuilder createFieldError(final String field, final String[] errorCodes) {
        
        final String fieldPath = buildFieldPath(field);
        final Class<?> fieldType = getFieldType(field);
        final Object fieldValue = getFieldValue(field);
        
        String[] codes = new String[0];
        for(String errorCode : errorCodes) {
            codes = Utils.concat(codes, generateMessageCodes(errorCode, fieldPath, fieldType));
        }
        
        return new InternalFieldErrorBuilder(this, getObjectName(), fieldPath, codes)
                .sheetName(getSheetName())
                .rejectedValue(fieldValue);
                
    }
    
    /**
     * 型変換失敗時のフィールエラー用のビルダを作成します。
     * @param field フィールドパス。
     * @param fieldType フィールドのクラスタイプ
     * @param rejectedValue 型変換に失敗した値
     * @return {@link FieldError}のインスタンスを組み立てるビルダクラス。
     */
    public InternalFieldErrorBuilder createFieldConversionError(final String field, final Class<?> fieldType, final Object rejectedValue) {
        
        final String fieldPath = buildFieldPath(field);
        final String[] codes = messageCodeGenerator.generateTypeMismatchCodes(getObjectName(), fieldPath, fieldType);
        
        return new InternalFieldErrorBuilder(this, getObjectName(), fieldPath, codes)
                .sheetName(getSheetName())
                .rejectedValue(rejectedValue)
                .conversionFailure(true);
        
        
    }
    
    /**
     * フィールドに対するフォーマッタを登録する。
     * @since 2.0
     * @param field フィールド名
     * @param fieldType フィールドのクラスタイプ
     * @param formatter フォーマッタ
     */
    public void registerFieldFormatter(final String field, final Class<?> fieldType, final FieldFormatter<?> formatter) {
        
        registerFieldFormatter(field, fieldType, formatter, false);
        
    }
    
    /**
     * フィールドに対するフォーマッタを登録する。
     * @since 2.0
     * @param field フィールド名
     * @param fieldType フィールドのクラスタイプ
     * @param formatter フォーマッタ
     * @param strippedIndex 登録するときにフィールドパスから、インデックス情報を除去するかどうか。
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void registerFieldFormatter(final String field, final Class<?> fieldType, final FieldFormatter<?> formatter,
            final boolean strippedIndex) {
        
        String fieldPath = buildFieldPath(field);
        
        if(strippedIndex) {
            // パスからインデックスやキーを削除する
            List<String> strippedPaths = new ArrayList<>();
            fieldFormatterRegistry.addStrippedPropertyPaths(strippedPaths, "", fieldPath);
            
            if(strippedPaths.size() > 0) {
                // 辞書順位並び変えて先頭に来るのが、インデックスを全て削除されたパス
                Collections.sort(strippedPaths);
                fieldPath = strippedPaths.get(0);
            }
        }
        
        fieldFormatterRegistry.registerFormatter(fieldPath, (Class)fieldType, (FieldFormatter)formatter);
        
    }
    
    /**
     * フィールドとクラスタイプを指定してフォーマッタを取得する。
     * @since 2.0
     * @param field フィールド名
     * @param fieldType フィールドのクラスタイプ
     * @return 見つからない場合は、nullを返す。
     */
    public <T> FieldFormatter<T> findFieldFormatter(final String field, final Class<T> fieldType) {
        String fieldPath = buildFieldPath(field);
        return fieldFormatterRegistry.findFormatter(fieldPath, fieldType);
    }
    
    public String[] generateMessageCodes(final String code) {
        return getMessageCodeGenerator().generateCodes(code, getObjectName());
    }
    
    public String[] generateMessageCodes(final String code, final String field) {
        return getMessageCodeGenerator().generateCodes(code, getObjectName(), field, null);
    }
    
    public String[] generateMessageCodes(final String code, final String field, final Class<?> fieldType) {
        return getMessageCodeGenerator().generateCodes(code, getObjectName(), field, fieldType);
    }
    
    public MessageCodeGenerator getMessageCodeGenerator() {
        return messageCodeGenerator;
    }
    
    public void setMessageCodeGenerator(MessageCodeGenerator messageCodeGenerator) {
        this.messageCodeGenerator = messageCodeGenerator;
    }
    
    /**
     * フィールドのフォーマッタの管理クラスを取得する。
     * @return
     */
    public FieldFormatterRegistry getFieldFormatterRegistry() {
        return fieldFormatterRegistry;
    }
    
    /**
     * フィールドのフォーマッタクラスを設定する。
     * @param fieldFormatterRegistry フィールドのフォーマッタの管理クラス
     */
    public void setFieldFormatterRegistry(FieldFormatterRegistry fieldFormatterRegistry) {
        this.fieldFormatterRegistry = fieldFormatterRegistry;
    }
    
}