AnnotationValueExtractor.java

/*
 * Copyright © 2017, Saleforce.com, Inc
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the <organization> nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.salesforce.aptspring.processor;

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

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.SimpleAnnotationValueVisitor8;

public class AnnotationValueExtractor {

  private static final String ALIAS_TYPE = "org.springframework.core.annotation.AliasFor";
  
  private static final String ALIAS_TARGET_TYPE = "annotation";

  private static final String ALIAS_TARGET_FIELD = "attribute";
  
  private static final String DEFAULT_ANNOTATION_VALUE = "value";

  
  private static class AliasData {
    private String targetAnnotation = null;
    private String targetField = null;
  }
  
  /**
   * Utility method to extract the value of annotation on a class.
   * Hooks to honor spring's AliasFor annotation, see {@link AnnotationValueExtractor#ALIAS_TYPE}.
   * 
   * @param e the element to inspect
   * @param annotationTypeName the fully qualified name of the annotation class.
   * @param methodName the name of the annotation value
   * @return an array of Strings representing the value of annotation parameter or it's alias.
   *     null if the annotation is not present (or is in a wrapper annotation as an array of values),
   *     an empty array is returned if the annotation is present, but the method does not exist.
   */
  public static String[] getAnnotationValue(Element e, String annotationTypeName, String methodName) {
    if (e instanceof TypeElement) {
      //TODO: do recursive call in to 
      ((TypeElement) e).getSuperclass();
      ((TypeElement) e).getInterfaces();
    }
    for (AnnotationMirror a : e.getAnnotationMirrors()) {
      String[] returned = getAnnotationValue(a, annotationTypeName, methodName);
      if (returned != null) {
        return returned;
      }
    }
    return null;
  }
  
  /**
   * Any empty array will be returned as long as the annotation is found (regardless of whether the value is set or not).
   * A null value is returned if the (meta) annotation is not found. Currently only supports one level of indirection through
   * spring's AliasFor.
   *
   * @param am the annotation to parse for a value.
   * @param annotationTypeName the type of the annotation we are interested in, necessary for meta-annotation processing.
   * @param methodName the name of the parameter designating the value 
   * @return if the annotation or meta annotation is found, the AnnotationValues are converted to strings by 
   *    {@link AnnotationValueExtractor} and returned in an array.  
   */
  private static String[] getAnnotationValue(AnnotationMirror am, String annotationTypeName, String methodName) {
    String currentType = am.getAnnotationType().toString();
    for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
      boolean aliasMatch = aliasMatch(getAlias(ev.getKey()), annotationTypeName, methodName, currentType);
      boolean foundField = ev.getKey().getSimpleName().toString().equals(methodName);
      if (aliasMatch || (foundField && currentType.equals(annotationTypeName))) {
        AnnotationValueExtractorVisitor ex = new AnnotationValueExtractorVisitor();
        List<String> values = new ArrayList<>();
        ex.visit(ev.getValue(), values);
        return values.toArray(new String[values.size()]); 
      }
    }
    if (currentType.equals(annotationTypeName)) {
      //no field matched
      return new String[]{};
    }
    
    for (AnnotationMirror a : am.getAnnotationType().getAnnotationMirrors()) {
      //cachable here...
      if (!a.getAnnotationType().asElement().toString().startsWith("java.lang.annotation")) {
        String[] output = getAnnotationValue(a, annotationTypeName, methodName);
        if (output != null) {
          return output;
        }
      }
    }
    return null;
  }
  
  
  /**
   * On an executable element (that is a value holder on annotation) extract any direct uses of @AlaisFor.
   * 
   * @param annotationParameter the annotation's parameter to inspect for uses of @AliasFor
   * @return an AliasData if the the annotation is found, null otherwise.
   */
  private static AliasData getAlias(ExecutableElement annotationParameter) {
    AliasData output = null;
    for (AnnotationMirror am : annotationParameter.getAnnotationMirrors()) {
      if (ALIAS_TYPE.equals(am.getAnnotationType().asElement().toString())) {
        output = new AliasData();
        for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
          String fieldName = ev.getKey().getSimpleName().toString();
          if (ALIAS_TARGET_TYPE.equals(fieldName)) {
            if (ev.getValue() != null && ev.getValue().getValue() != null) {
              output.targetAnnotation = ev.getValue().getValue().toString();
            } else {
              return null;
            }
          }
          if (ALIAS_TARGET_FIELD.equals(fieldName) 
              && ev.getValue() != null && ev.getValue().getValue() != null) {
            output.targetField = ev.getValue().getValue().toString();
          }
          if (DEFAULT_ANNOTATION_VALUE.equals(fieldName)
              && (ev.getValue() != null && ev.getValue().getValue() != null)) {
            output.targetField = ev.getValue().getValue().toString();
          }
        }
      }
    }
    return output;
  }
  
  /**
   *  Checks to see if the aliasData matches the targetType and targetField.   The aliasData may have a null
   *  targetType and if so, the currentAnnotation is used to determine if the targetType Matches.
   *  This indicates that the AliasFor annotation is on an element in the targetType annotation itself.
   */
  private static boolean aliasMatch(AliasData aliasData, String targetType, String targetField, String currentAnnotation) {
    if (aliasData == null) {
      return false;
    }
    return (//types match
        (targetType.equals(aliasData.targetAnnotation)
        || (aliasData.targetAnnotation == null && targetType.equals(currentAnnotation)))
        && //fields match
        targetField.equals(aliasData.targetField));
  }
  
  private static class AnnotationValueExtractorVisitor extends SimpleAnnotationValueVisitor8<Void, List<String>> {

    @Override
    protected Void defaultAction(Object o, List<String> values) {
      values.add(o.toString());
      return null;
    }

    public Void visitEnumConstant(VariableElement c, List<String> values) {
      values.add(c.getSimpleName().toString());
      return null;
    }

    public Void visitAnnotation(AnnotationMirror a, List<String> values) {
      // should probably do something here, but what? return annotation types?
      return defaultAction(a, values);
    }

    public Void visitArray(List<? extends AnnotationValue> vals, List<String> values) {
      for (AnnotationValue val : vals) {
        visit(val, values);
      }
      return null;
    }
  }
}