PropertyTypeNavigator.java

package com.gh.mygreen.xlsmapper.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.gh.mygreen.xlsmapper.util.PropertyPath.Token;

/**
 * クラス定義からプロパティのクラスタイプを取得する。
 * <p>プロパティは式言語の形式に似た形式をとることが可能で、フィールドにもアクセスできます。</p>
 * 
 * 
 * @since 2.0
 * @author T.TSUCHIE
 *
 */
public class PropertyTypeNavigator {
    
    /**
     * 非公開のプロパティにアクセス可能かどうか。
     */
    private boolean allowPrivate;
    
    /**
     * クラスタイプの解析ができない場合に処理を終了するかどうか。
     */
    private boolean ignoreNotResolveType;
    
    /**
     * プロパティの解析結果をキャッシュするかどうか。
     */
    private boolean cacheWithPath;
    
    /**
     * プロパティの式を解析して、トークンに分解するクラス。
     */
    private final PropertyPathTokenizer tokenizer = new PropertyPathTokenizer();
    
    /**
     * プロパティの解析結果をキャッシュデータ
     */
    private final Map<String, PropertyPath> cacheData = new ConcurrentHashMap<>();
    
     /**
     * プロパティの値を取得する。
     * <p>オプションはデフォルト値で処理する。</p>
     * @param rootClass 取得元となるクラス
     * @param property プロパティの式。
     * @return プロパティのクラスタイプ。
     * @throws IllegalArgumentException peropety is null or empty.
     * @throws PropertyAccessException 存在しないプロパティを指定した場合など。
     * @throws IllegalStateException リストやマップにアクセスする際にGenericsタイプが設定されておらずクラスタイプが取得できない場合。
     *         ただし、オプションignoreNotResolveType = falseのとき。
     */
    public static Object get(final Class<?> rootClass, final String property) {
        return new PropertyTypeNavigator().getPropertyType(rootClass, property);
    }
    
    /**
     * デフォルトコンストラクタ
     */
    public PropertyTypeNavigator() {
        
    }
    
    /**
     * プロパティの値を取得する。
     * 
     * @param rootClass 取得元となるクラス
     * @param property プロパティの式。
     * @return プロパティのクラスタイプ。
     * @throws IllegalArgumentException peropety is null or empty.
     * @throws PropertyAccessException 存在しないプロパティを指定した場合など。
     * @throws IllegalStateException リストやマップにアクセスする際にGenericsタイプが設定されておらずクラスタイプが取得できない場合。
     *         ただし、オプションignoreNotResolveType = falseのとき。
     */
    public Class<?> getPropertyType(final Class<?> rootClass, final String property) {
        
        ArgUtils.notEmpty(property, "property");
        
        final PropertyPath path = parseProperty(property);
        final LinkedList<Object> stack = new LinkedList<>();
        Class<?> targetClass = rootClass;
        for(Token token : path.getPathAsToken()) {
            
            targetClass = accessProperty(targetClass, token, stack);
            if(targetClass == null) {
                return null;
            }
        }
        
        return targetClass;
        
    }
    
    /**
     * プロパティの式をパースして、{@link PropertyPath} オブジェクトに変換する。
     * @param property プロパティアクセス用の式
     * @return 式を解析した結果 {@link PropertyPath}
     */
    private PropertyPath parseProperty(final String property) {
        
        if(isCacheWithPath()) {
            return cacheData.computeIfAbsent(property, k -> tokenizer.parse(k));
        } else {
            return tokenizer.parse(property);
        }
        
    }
    
    private Class<?> accessProperty(final Class<?> targetClass, final Token token, final LinkedList<Object> stack) {
        
        if(token instanceof Token.Separator) {
            return accessPropertyBySeparator(targetClass, (Token.Separator)token, stack);
            
        } else if(token instanceof Token.Name) {
            return accessPropertyByName(targetClass, (Token.Name)token, stack);
            
        } else if(token instanceof Token.Key) {
            return accessPropertyByKey(targetClass, (Token.Key)token, stack);
        }
        
        throw new IllegalStateException("not support token type : " + token.getValue());
        
    }
    
    private Class<?> accessPropertyBySeparator(final Class<?> targetClass, final Token.Separator token, final LinkedList<Object> stack) {
        
        return targetClass;
        
    }
    
    private Class<?> accessPropertyByName(final Class<?> targetClass, final Token.Name token, final LinkedList<Object> stack) {
        
        // メソッドアクセス
        final String getterMethodName = "get" + Utils.capitalize(token.getValue());
        try {
            Method getterMethod = allowPrivate ? 
                    targetClass.getDeclaredMethod(getterMethodName) : targetClass.getMethod(getterMethodName);
            getterMethod.setAccessible(true);
            
            // Collectionなどの場合、メソッド情報をスタックに積んでおく
            stack.push(getterMethod);
            
            return getterMethod.getReturnType();
            
        } catch (NoSuchMethodException | SecurityException e) {
            // not found method
            
        }
        
        // boolean用メソッドアクセス
        final String booleanMethodName = "is" + Utils.capitalize(token.getValue());
        try {
            Method getterMethod = allowPrivate ? 
                    targetClass.getDeclaredMethod(booleanMethodName) : targetClass.getMethod(booleanMethodName);;
            getterMethod.setAccessible(true);
            
            return getterMethod.getReturnType();
            
        } catch (NoSuchMethodException | SecurityException e) {
            // not found method
            
        }
        
        // フィールドアクセス
        final String fieldName = token.getValue();
        try {
            Field field = allowPrivate ? 
                    targetClass.getDeclaredField(fieldName) : targetClass.getField(fieldName);
            field.setAccessible(true);
            
            // Collectionなどの場合、メソッド情報をスタックに積んでおく
            stack.push(field);
            
            return field.getType();
            
        } catch (NoSuchFieldException | SecurityException e) {
            // not found field
            
        }
        
        throw new PropertyAccessException("not found property : " + token.getValue());
        
    }
    
    private Class<?> accessPropertyByKey(final Class<?> targetClass, final Token.Key token, final LinkedList<Object> stack) {
        
        final Object parent = stack.isEmpty() ? null : stack.pollFirst();
        if(parent == null) {
            return null;
        }
        if(Collection.class.isAssignableFrom(targetClass)) {
            
            Type argType = null;
            if(parent instanceof Method) {
                // getterメソッドのからGenericsのタイプを取得する
                Type type = ((Method)parent).getGenericReturnType();
                if(!ParameterizedType.class.isAssignableFrom(type.getClass())) {
                    if(ignoreNotResolveType) {
                        return null;
                    } else {
                        throw new IllegalStateException("not resolve generics type with property : " + token.getValue());
                    }
                }
                argType = ((ParameterizedType)type).getActualTypeArguments()[0];
                
            } else if(parent instanceof Field) {
                Type type = ((Field)parent).getGenericType();
                if(!ParameterizedType.class.isAssignableFrom(type.getClass())) {
                    if(ignoreNotResolveType) {
                        return null;
                    } else {
                        throw new IllegalStateException("not resolve generics type with property : " + token.getValue());
                    }
                }
                
                argType = ((ParameterizedType)type).getActualTypeArguments()[0];
                
            } else if(parent instanceof ParameterizedType) {
                ParameterizedType type = (ParameterizedType) parent;
                argType = type.getActualTypeArguments()[0];
                
            } else {
                if(ignoreNotResolveType) {
                    return null;
                } else {
                    throw new IllegalStateException("not resolve generics type with property : " + token.getValue());
                }
            }
            
            if(argType == null) {
                return null;
            }
            
            if(argType instanceof Class) {
                return (Class<?>)argType;
                
            } else if(argType instanceof ParameterizedType) {
                ParameterizedType paramType = ((ParameterizedType)argType);
                stack.push(paramType);
                return (Class<?>)paramType.getRawType();
            }
            
        } else if (targetClass.isArray()) {
            
            return targetClass.getComponentType();
            
        } else if(Map.class.isAssignableFrom(targetClass)) {
            
            Type argType = null;
            if(parent instanceof Method) {
                // getterメソッドのからGenericsのタイプを取得する
                Type type = ((Method)parent).getGenericReturnType();
                if(!ParameterizedType.class.isAssignableFrom(type.getClass())) {
                    if(ignoreNotResolveType) {
                        return null;
                    } else {
                        throw new IllegalStateException("not resolve generics type with property : " + token.getValue());
                    }
                }
                argType = ((ParameterizedType)type).getActualTypeArguments()[1];
                
            } else if(parent instanceof Field) {
                Type type = ((Field)parent).getGenericType();
                if(!ParameterizedType.class.isAssignableFrom(type.getClass())) {
                    if(ignoreNotResolveType) {
                        return null;
                    } else {
                        throw new IllegalStateException("not resolve generics type with property : " + token.getValue());
                    }
                }
                argType = ((ParameterizedType)type).getActualTypeArguments()[1];
                
            } else if(parent instanceof ParameterizedType) {
                ParameterizedType type = (ParameterizedType) parent;
                argType = type.getActualTypeArguments()[1];
                
            } else {
                if(ignoreNotResolveType) {
                    return null;
                } else {
                    throw new IllegalStateException("not resolve generics type with property : " + token.getValue());
                }
            }
            
            if(argType == null) {
                return null;
            }
            
            if(argType instanceof Class) {
                return (Class<?>)argType;
                
            } else if(argType instanceof ParameterizedType) {
                ParameterizedType paramType = ((ParameterizedType)argType);
                stack.push(paramType);
                return (Class<?>)paramType.getRawType();
            }
            
        }
        
        throw new PropertyAccessException("not support key access : " + targetClass.getName());
        
    }
    
    /**
     * 今までのキャッシュデータをクリアする。
     */
    public void clearCache() {
        this.cacheData.clear();
    }
    
    /**
     * 実行時オプション - 非公開のプロパティにアクセス可能かどうか。
     * @return true: アクセスを許可する。false: デフォルト値。
     */
    public boolean isAllowPrivate() {
        return allowPrivate;
    }
    
    /**
     * 実行時オプション - 非公開のプロパティにアクセス可能かどうか。
     * @param allowPrivate true: アクセスを許可する。
     */
    public void setAllowPrivate(boolean allowPrivate) {
        this.allowPrivate = allowPrivate;
    }
    
    /**
     * クラスタイプの解析ができない場合に処理を終了するかどうか。
     * <p>ListなどでGenericsタイプが指定されていでクラスタイプが取得できないときに、nullを返すかどうか指定します。</p>
     * @return true:その時点で処理を終了する。
     */
    public boolean isIgnoreNotResolveType() {
        return ignoreNotResolveType;
    }
    
    /**
     * クラスタイプの解析ができない場合に処理を終了するかどうか。
     * <p>ListなどでGenericsタイプが指定されていでクラスタイプが取得できないときに、nullを返すかどうか指定します。</p>
     * @param ignoreNotResolveType true:その時点で処理を終了する。
     */
    public void setIgnoreNotResolveType(boolean ignoreNotResolveType) {
        this.ignoreNotResolveType = ignoreNotResolveType;
    }
    
    
    /**
     * プロパティの解析結果をキャッシュするかどうか。
     * @return true: キャッシュする。false: デフォルト値。
     */
    public boolean isCacheWithPath() {
        return cacheWithPath;
    }
    
    /**
     * プロパティの解析結果をキャッシュするかどうか。
     * @param cacheWithPath true: キャッシュする。
     */
    public void setCacheWithPath(boolean cacheWithPath) {
        this.cacheWithPath = cacheWithPath;
    }
    
}