AnnotationExpander.java

package com.github.mygreen.supercsv.builder;

import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.supercsv.exception.SuperCsvReflectionException;

import com.github.mygreen.supercsv.annotation.CsvComposition;
import com.github.mygreen.supercsv.annotation.CsvOverridesAttribute;
import com.github.mygreen.supercsv.annotation.constraint.CsvConstraint;
import com.github.mygreen.supercsv.annotation.constraint.CsvRequire;
import com.github.mygreen.supercsv.annotation.conversion.CsvConversion;
import com.github.mygreen.supercsv.exception.SuperCsvInvalidAnnotationException;
import com.github.mygreen.supercsv.localization.MessageBuilder;
import com.github.mygreen.supercsv.util.Utils;

/**
 * 繰り返しのアノテーション、合成のアノテーションを考慮して、アノテーションを展開します。
 * <p>並び順は、コンストラクタで指定されたものに並び変えられる。</p>
 * 
 * <p>繰り返しのアノテーション{@link Repeatable}が付与されたアノテーションの場合、
 *    取得する際には複数のアノテーションがまとめたアノテーションとして別に取得されるため分解する。</p>
 * 
 * <p>合成のアノテーション{@link CsvComposition}が付与されたアノテーションの場合、
 *    付与されているアノテーションに分解する。
 *    その際に、定義されている属性を元に、付与されているアノテーションの属性を上書きする。</p>
 *
 * @since 2.0
 * @author T.TSUCHIE
 *
 */
public class AnnotationExpander {
    
    private final Comparator<ExpandedAnnotation> comparator;
    
    /**
     * アノテーションの並び順を指定するコンストラクタ。
     * 
     * @param annotationComparator アノテーションの並び順を指定するためのComparator。
     * @throws NullPointerException {@literal annotationComparator == null.}
     */
    public AnnotationExpander(final Comparator<Annotation> annotationComparator) {
        Objects.requireNonNull(annotationComparator);
        
        this.comparator = new Comparator<ExpandedAnnotation>() {
            
            @Override
            public int compare(final ExpandedAnnotation o1, final ExpandedAnnotation o2) {
                return annotationComparator.compare(o1.getOriginal(), o2.getOriginal());
            }
            
        };
    }
    
    /**
     * 複数のアノテーションを展開する。
     * @param targetAnnos  展開対象のアノテーション
     * @return 展開されたアノテーション
     * @throws NullPointerException {@literal targetAnnos == null.}
     */
    public List<ExpandedAnnotation> expand(final Annotation[] targetAnnos) {
        Objects.requireNonNull(targetAnnos);
        
        final List<ExpandedAnnotation> expanedList = new ArrayList<>();
        for(Annotation targetAnno : targetAnnos) {
            expanedList.addAll(expand(targetAnno));
        }
        
        Collections.sort(expanedList, comparator);
        
        return expanedList;
        
    }
    
    /**
     * アノテーションを展開する。
     * @param targetAnno 展開対象のアノテーション
     * @return 展開されたアノテーション
     * @throws NullPointerException {@literal targetAnno == null.}
     */
    public List<ExpandedAnnotation> expand(final Annotation targetAnno) {
        Objects.requireNonNull(targetAnno);
        
        final List<ExpandedAnnotation> expandedList = new ArrayList<>();
        
        if(isRepeated(targetAnno)) {
            // 繰り返しのアノテーションの場合、要素を抽出する。
            try {
                final Method method = targetAnno.getClass().getMethod("value");
                final Annotation[] annos = (Annotation[]) method.invoke(targetAnno);
                
                int index = 0;
                for(Annotation anno : annos) {
                    final List<ExpandedAnnotation> repeatedAnnos = expand(anno);
                    for(ExpandedAnnotation repeatedAnno : repeatedAnnos) {
                        repeatedAnno.setIndex(index);
                    }
                    
                    expandedList.addAll(repeatedAnnos);
                    index++;
                }
                
            } catch (Exception e) {
                throw new RuntimeException("fail get repeated value attribute.", e);
            }
            
        } else if(isComposed(targetAnno)) {
            final ExpandedAnnotation composedAnno = new ExpandedAnnotation(targetAnno, true);
            
            // 合成のアノテーションの場合、メタアノテーションを子供としてさらに抽出する。
            final List<Annotation> childAnnos = Arrays.asList(targetAnno.annotationType().getAnnotations());
            for(Annotation anno : childAnnos) {
                
                final List<ExpandedAnnotation> nestedAnnos = expand(anno).stream()
                        .map(nestedAnno -> overrideAttribute(targetAnno, nestedAnno))
                        .collect(Collectors.toList());
                
                composedAnno.addChilds(nestedAnnos);
                
            }
            
            Collections.sort(composedAnno.getChilds(), comparator);
            expandedList.add(composedAnno);
            
        } else {
            // 通常のアノテーションの場合
            expandedList.add(new ExpandedAnnotation(targetAnno, false));
            
        }
        
        Collections.sort(expandedList, comparator);
        return expandedList;
        
    }
    
    /**
     * 繰り返されたアノテーションかどうか判定する。
     * <p>属性「value」に、繰り返しのアノテーション{@link Repeatable}が付与されている
     *    アノテーションの配列を保持しているかどうかで判定する。</p>
     * @param targetAnno
     * @return
     */
    private boolean isRepeated(final Annotation targetAnno) {
        
        try {
            final Method method = targetAnno.getClass().getMethod("value");
            
            // 値のクラスタイプがアノテーションの配列かどうかのチェック
            final Class<?> returnType = method.getReturnType();
            if(!(returnType.isArray() && Annotation.class.isAssignableFrom(returnType.getComponentType()))) {
                return false;
            }
            
            final Annotation[] annos = (Annotation[]) method.invoke(targetAnno);
            if(annos.length == 0) {
                return false;
            }
            
            // @Repetableアノテーションが付与されているかどうか
            if(annos[0].annotationType().getAnnotation(Repeatable.class) != null) {
                return true;
            }
            
        } catch (Exception e) {
            
        }
        
        return false;
        
    }
    
    /**
     * 合成されたアノテーションかどうか判定する。
     * <p>メタアノテーション{@link CsvComposition}が付与されているかどうかで判定する。</p>
     * @param targetAnno
     * @return
     */
    private boolean isComposed(final Annotation targetAnno) {
        
        return targetAnno.annotationType().getAnnotation(CsvComposition.class) != null;
        
    }
    
    /**
     * 合成したアノテーションの属性を、構成されるアノテーションに反映する。
     * @param compositionAnno 
     * @param nestedAnno
     * @return
     */
    private ExpandedAnnotation overrideAttribute(final Annotation compositionAnno, final ExpandedAnnotation nestedAnno) {
        
        final Annotation originalAnno = nestedAnno.getOriginal();
        if(!isOverridableAnnotation(originalAnno)) {
            return nestedAnno;
        }
        
        // 上書きするアノテーションの属性の組み立て
        final Map<String, Object> overrideAttrs = buildOverrideAttribute(compositionAnno, nestedAnno);
        if(overrideAttrs.isEmpty()) {
            return nestedAnno;
        }
        
        // 既存のアノテーションの属性の作成
        final Class<?> annotationClass = originalAnno.annotationType();
        final Map<String, Object> defaultValues = new HashMap<>();
        for(Method method : annotationClass.getMethods()) {
            try {
                method.setAccessible(true);
                if(method.getParameterCount() == 0) {
                    final Object value = method.invoke(originalAnno);
                    defaultValues.put(method.getName(), value);
                    
                } else {
                    final Object value = method.getDefaultValue();
                    if(value != null) {
                        defaultValues.put(method.getName(), value);
                    }
                    
                }
                
            } catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                throw new RuntimeException(String.format("fail get annotation attribute %s#%s.", annotationClass.getName(), method.getName()), e);
            }
        }
        
        // アノテーションのインスタンスの組み立てなおし
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        final Object annoObj = Proxy.newProxyInstance(classLoader, new Class[]{annotationClass},
                new InvocationHandler() {
                    
                    @Override
                    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
                        final String name = method.getName();
                        if (name.equals("annotationType")) {
                            return annotationClass;
                            
                        } else if(overrideAttrs.containsKey(name)){
                            return overrideAttrs.get(name);
                            
                        } else {
                            return defaultValues.get(name);
                        }
                    }
        });
        
        // 値をコピーする
        final ExpandedAnnotation propagatedAnno = new ExpandedAnnotation((Annotation)annoObj, nestedAnno.isComposed());
        propagatedAnno.setIndex(nestedAnno.getIndex());
        propagatedAnno.addChilds(nestedAnno.getChilds());
        
        return propagatedAnno;
    }
    
    /**
     * アノテーションの属性を上書き可能かか判定する。
     * <p>ただし、実際には合成のアノテーションに属性がなければ上書きはされないので、あくまで上書き可能かの判定しか行わない。</p>
     * <p>条件は以下の通り。</p>
     * <ul>
     *  <li>メタアノテーション{@link CsvConstraint}、{@link CsvConversion}が付与されているアノテーションである。</li>
     *  <li>アノテーション{@link CsvRequire}である。</li>
     *  <li>フォーマット用のアノテーションである。パッケージ名から判定する。</li>
     * </ul>
     * @param targetAnno 判定対象のアノテーション
     * @return trueの場合、上書き対象。
     */
    private boolean isOverridableAnnotation(final Annotation targetAnno) {
        
        final Class<?> annoType = targetAnno.annotationType();
        if(annoType.getAnnotation(CsvConstraint.class) != null) {
            return true;
        }
        
        if(annoType.getAnnotation(CsvConversion.class) != null) {
            return true;
        }
        
        if(annoType.getTypeName().startsWith("com.github.mygreen.supercsv.annotation.format")) {
            return true;
        }
        
        return false;
        
    }
    
    /**
     * 上書きする属性の組み立て
     * @param compositionAnno 合成のアノテーション
     * @param targetAnno 上書き対象のアノテーション
     * @return
     */
    @SuppressWarnings("rawtypes")
    private Map<String, Object> buildOverrideAttribute(final Annotation compositionAnno, final ExpandedAnnotation targetAnno) {
        
        final Annotation originalAnno = targetAnno.getOriginal();
        
        final Map<String, Object> overrideAttrs = new HashMap<>();
        
        // @CsvOvrerideAttributeが付与されたメソッド名
        final Set<String> overrideMethodNames = new HashSet<>();
        
        // @CsvOverridesAttributeが付与された属性の組み立て
        for(Method compositionMethod : compositionAnno.annotationType().getMethods()) {
            
            final List<CsvOverridesAttribute> annoList = new ArrayList<>();
            final CsvOverridesAttribute overrideAttrAnno = compositionMethod.getAnnotation(CsvOverridesAttribute.class);
            if(overrideAttrAnno != null) {
                annoList.add(overrideAttrAnno);
            }
            
            // 繰り返しのアノテーションの場合
            final CsvOverridesAttribute.List overrideAttrAnnoList = compositionMethod.getAnnotation(CsvOverridesAttribute.List.class);
            if(overrideAttrAnnoList != null) {
                annoList.addAll(Arrays.asList(overrideAttrAnnoList.value()));
            }
            
            if(annoList.isEmpty()) {
                // @CsvOverridesAttributeが付与されていない場合
                continue;
            }
            
            overrideMethodNames.add(compositionMethod.getName());
            
            for(CsvOverridesAttribute anno : annoList) {
                // アノテーションのクラスの判定
                if(!anno.annotation().equals(originalAnno.annotationType())) {
                    continue;
                }
                
                // インデックスの判定
                if(anno.index() >= 0 && anno.index() != targetAnno.getIndex()) {
                    continue;
                }
                
                final String attrName = anno.name().isEmpty() ? compositionMethod.getName() : anno.name();
                
                if(!Utils.hasAnnotationAttribute(originalAnno, attrName, compositionMethod.getReturnType())) {
                    // 上書き対象の属性が見つからない場合
                    throw new SuperCsvInvalidAnnotationException(originalAnno, MessageBuilder.create("anno.CsvOverridesAnnotation.notFoundAttr")
                            .varWithAnno("compositionAnno", compositionAnno.annotationType())
                            .varWithAnno("overrideAnno", originalAnno.annotationType())
                            .varWithClass("attrType", compositionMethod.getReturnType())
                            .var("attrName", attrName)
                            .format());
                    
                }
                
                // 属性値の取得
                try {
                    Object attrValue = compositionMethod.invoke(compositionAnno);
                    overrideAttrs.put(attrName, attrValue);
                    
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                    throw new SuperCsvReflectionException(MessageBuilder.create("anno.CsvOverridesAnnotation.failGetAttr")
                            .varWithAnno("compositionAnno", compositionAnno.annotationType())
                            .var("attrName", attrName)
                            .format(),
                            e);
                }
            
            }
            
        }
        
        // message 属性の取得。
        // 既に取得していたり、@CsvOverridesAttributeが付与されている場合はスキップする。
        if(!overrideAttrs.containsKey("message") && !overrideMethodNames.contains("message")
                && Utils.hasAnnotationAttribute(originalAnno, "message", String.class)) {
            
            final Optional<String> messageAttr = Utils.getAnnotationAttribute(compositionAnno, "message", String.class);
            if(messageAttr.isPresent() && Utils.isNotEmpty(messageAttr.get())) {
                overrideAttrs.put("message", messageAttr.get());
            }
        }
        
        // groups 属性の取得。
        // 既に取得していたり、@CsvOverridesAttributeが付与されている場合はスキップする。
        if(!overrideAttrs.containsKey("groups") && !overrideMethodNames.contains("groups")
                && Utils.hasAnnotationAttribute(originalAnno, "groups", Class[].class)) {
            
            final Optional<Class[]> groupsAttr = Utils.getAnnotationAttribute(compositionAnno, "groups", Class[].class);
            if(groupsAttr.isPresent() && Utils.isNotEmpty(groupsAttr.get())) {
                overrideAttrs.put("groups", groupsAttr.get());
            }
        }
        
        // cases 属性の取得。
        // 既に取得していたり、@CsvOverridesAttributeが付与されている場合はスキップする。
        if(!overrideAttrs.containsKey("cases") && !overrideMethodNames.contains("cases")
                && Utils.hasAnnotationAttribute(originalAnno, "cases", BuildCase[].class)) {
            
            final Optional<BuildCase[]> casesAttr = Utils.getAnnotationAttribute(compositionAnno, "cases", BuildCase[].class);
            if(casesAttr.isPresent() && Utils.isNotEmpty(casesAttr.get())) {
                overrideAttrs.put("cases", casesAttr.get());
            }
        }
        
        return overrideAttrs;
    }
    
}