FieldFormatterRegistry.java

package com.gh.mygreen.xlsmapper.validation.fieldvalidation;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.gh.mygreen.xlsmapper.util.ArgUtils;


/**
 * {@link FieldFormatter}を管理するためのクラス。
 * <p>フィールドの値を検証する際に、フィールドの値をフォーマットしてエラーメッセージに埋め込むために使用します。</p>
 * 
 * @since 2.0
 * @author T.TSUCHIE
 *
 */
public class FieldFormatterRegistry {
    
    /**
     * クラスタイプで関連づけられたフォーマッタ
     */
    private Map<Class<?>, FieldFormatter<?>> typeMap = new HashMap<>();
    
    /**
     * フィールドパスで関連づけられたフォーマッタ
     */
    private Map<String, FormatterHolder> pathMap = new HashMap<>();
    
    /**
     * 登録されているフォーマッタを初期化する。
     */
    public void init() {
        this.typeMap.clear();
        this.pathMap.clear();
        
        
    }
    
    /**
     * クラスタイプを指定してフォーマッタを登録する。
     * @param <T> 処理対象のタイプ
     * @param requiredType クラスタイプ
     * @param formatter 登録対象のフォーマッタ
     * @throws IllegalArgumentException {@literal requiredType == null or formatter == null}
     */
    public <T> void registerFormatter(final Class<T> requiredType, FieldFormatter<T> formatter) {
        ArgUtils.notNull(requiredType, "requiredType");
        ArgUtils.notNull(formatter, "formatter");
        
        this.typeMap.put(requiredType, formatter);
        
    }
    
    /**
     * フィールドパスとクラスタイプを指定してフォーマッタを登録する。
     * @param <T> 処理対象のタイプ
     * @param fieldPath
     * @param requiredType クラスタイプ
     * @param formatter 登録対象のフォーマッタ
     */
    public <T> void registerFormatter(final String fieldPath, final Class<T> requiredType, FieldFormatter<T> formatter) {
        ArgUtils.notEmpty(fieldPath, "fieldPath");
        ArgUtils.notNull(requiredType, "requiredType");
        ArgUtils.notNull(formatter, "formatter");
        
        this.pathMap.put(fieldPath, new FormatterHolder(requiredType, formatter));
        
    }
    
    /**
     * 指定したクラスタイプに対する{@link FieldFormatter}を取得する。
     * @param <T> クラスタイプ
     * @param requiredType 取得したいクラスタイプ
     * @return 見つからない場合は、nullを返す。
     */
    @SuppressWarnings("unchecked")
    public <T> FieldFormatter<T> getFormatter(final Class<T> requiredType) {
        return (FieldFormatter<T>)typeMap.get(requiredType);
    }
    
    /**
     * 指定したパスとクラスタイプに対する{@link FieldFormatter}を取得する。
     * <p>引数「fieldPath」に完全に一致するフォーマッタを取得します。</p>
     * @param fieldPath フィールドパス。
     * @param requiredType 取得したいクラスタイプ
     * @return 見つからない場合は、nullを返す。
     */
    public <T> FieldFormatter<T> getFormatter(final String fieldPath, final Class<T> requiredType) {
        FormatterHolder holder = pathMap.get(fieldPath);
        return holder != null ? holder.getFormatter(requiredType) : null;
    }
    
    /**
     * 指定したパスとクラスタイプに対する{@link FieldFormatter}を取得する。
     * <p>ただし、リストのインデックスやキーが含まれている場合、引数「fieldPath」からそれらを取り除いた値で比較する。
     *  <br>一致するフィールドに対するフォーマッタが見つからない場合は、タイプのみで比較する。
     * </p>
     * @param fieldPath フィールドパス。
     * @param requiredType 取得したいクラスタイプ
     * @return 見つからない場合は、nullを返す。
     */
    public <T> FieldFormatter<T> findFormatter(final String fieldPath, final Class<T> requiredType) {
        
        // 完全なパスで比較
        FieldFormatter<T> formatter = getFormatter(fieldPath, requiredType);
        if(formatter != null) {
            return formatter;
        }
        
        // インデックスを除去した形式で比較
        final List<String> strippedPaths = new ArrayList<>();
        addStrippedPropertyPaths(strippedPaths, "", fieldPath);
        
        for(String strippedPath : strippedPaths) {
            formatter = getFormatter(strippedPath, requiredType);
            if(formatter != null) {
                return formatter;
            }
            
        }
        
        // 見つからない場合は、タイプのみで比較した物を取得する
        return getFormatter(requiredType);
        
        
    }
    
    /**
     * パスからリストのインデックス([1])やマップのキー([key])を除去したものを構成する。
     * <p>SpringFrameworkの「PropertyEditorRegistrySupport#addStrippedPropertyPaths(...)」の処理</p>
     * @param strippedPaths 除去したパス
     * @param nestedPath 現在のネストしたパス
     * @param propertyPath 処理対象のパス
     */
    public void addStrippedPropertyPaths(List<String> strippedPaths, String nestedPath, String propertyPath) {
        
        final int startIndex = propertyPath.indexOf('[');
        if (startIndex != -1) {
            final int endIndex = propertyPath.indexOf(']');
            if (endIndex != -1) {
                final String prefix = propertyPath.substring(0, startIndex);
                final String key = propertyPath.substring(startIndex, endIndex + 1);
                final String suffix = propertyPath.substring(endIndex + 1, propertyPath.length());
                
                // Strip the first key.
                strippedPaths.add(nestedPath + prefix + suffix);
                
                // Search for further keys to strip, with the first key stripped.
                addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix);
                
                // Search for further keys to strip, with the first key not stripped.
                addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix);
            }
        }
    }
    
    private static class FormatterHolder {
        
        private final Class<?> registerdType;
        
        private final FieldFormatter<?> formatter;
        
        private FormatterHolder(Class<?> registerdType, FieldFormatter<?> formatter) {
            this.registerdType = registerdType;
            this.formatter = formatter;
        }
        
        @SuppressWarnings("unchecked")
        private <T> FieldFormatter<T> getFormatter(final Class<T> requiredType) {
            if(registerdType.isAssignableFrom(requiredType)) {
                return (FieldFormatter<T>)formatter;
            } else {
                return null;
            }
        }
    }
    
}