View Javadoc
1   /*
2    * Copyright © 2017, Salesforce.com, Inc
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions are met:
7    *     * Redistributions of source code must retain the above copyright
8    *       notice, this list of conditions and the following disclaimer.
9    *     * Redistributions in binary form must reproduce the above copyright
10   *       notice, this list of conditions and the following disclaimer in the
11   *       documentation and/or other materials provided with the distribution.
12   *     * Neither the name of the <organization> nor the
13   *       names of its contributors may be used to endorse or promote products
14   *       derived from this software without specific prior written permission.
15   *
16   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19   * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
20   * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   */
27  package com.salesforce.aptspring.processor;
28  
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Map.Entry;
32  
33  import javax.lang.model.element.AnnotationMirror;
34  import javax.lang.model.element.AnnotationValue;
35  import javax.lang.model.element.Element;
36  import javax.lang.model.element.ExecutableElement;
37  import javax.lang.model.element.TypeElement;
38  import javax.lang.model.element.VariableElement;
39  import javax.lang.model.util.SimpleAnnotationValueVisitor8;
40  
41  public class AnnotationValueExtractor {
42  
43    private static final String ALIAS_TYPE = "org.springframework.core.annotation.AliasFor";
44    
45    private static final String ALIAS_TARGET_TYPE = "annotation";
46  
47    private static final String ALIAS_TARGET_FIELD = "attribute";
48    
49    private static final String DEFAULT_ANNOTATION_VALUE = "value";
50  
51    
52    private static class AliasData {
53      private String targetAnnotation = null;
54      private String targetField = null;
55    }
56    
57    /**
58     * Utility method to extract the value of annotation on a class.
59     * Hooks to honor spring's AliasFor annotation, see {@link AnnotationValueExtractor#ALIAS_TYPE}.
60     * 
61     * @param e the element to inspect
62     * @param annotationTypeName the fully qualified name of the annotation class.
63     * @param methodName the name of the annotation value
64     * @return an array of Strings representing the value of annotation parameter or it's alias.
65     *     null if the annotation is not present (or is in a wrapper annotation as an array of values),
66     *     an empty array is returned if the annotation is present, but the method does not exist.
67     */
68    public static String[] getAnnotationValue(Element e, String annotationTypeName, String methodName) {
69      if (e instanceof TypeElement) {
70        //TODO: do recursive call in to 
71        ((TypeElement) e).getSuperclass();
72        ((TypeElement) e).getInterfaces();
73      }
74      for (AnnotationMirror a : e.getAnnotationMirrors()) {
75        String[] returned = getAnnotationValue(a, annotationTypeName, methodName);
76        if (returned != null) {
77          return returned;
78        }
79      }
80      return null;
81    }
82    
83    /**
84     * Any empty array will be returned as long as the annotation is found (regardless of whether the value is set or not).
85     * A null value is returned if the (meta) annotation is not found. Currently only supports one level of indirection through
86     * spring's AliasFor.
87     *
88     * @param am the annotation to parse for a value.
89     * @param annotationTypeName the type of the annotation we are interested in, necessary for meta-annotation processing.
90     * @param methodName the name of the parameter designating the value 
91     * @return if the annotation or meta annotation is found, the AnnotationValues are converted to strings by 
92     *    {@link AnnotationValueExtractor} and returned in an array.  
93     */
94    private static String[] getAnnotationValue(AnnotationMirror am, String annotationTypeName, String methodName) {
95      String currentType = am.getAnnotationType().toString();
96      for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
97        boolean aliasMatch = aliasMatch(getAlias(ev.getKey()), annotationTypeName, methodName, currentType);
98        boolean foundField = ev.getKey().getSimpleName().toString().equals(methodName);
99        if (aliasMatch || (foundField && currentType.equals(annotationTypeName))) {
100         AnnotationValueExtractorVisitor ex = new AnnotationValueExtractorVisitor();
101         List<String> values = new ArrayList<>();
102         ex.visit(ev.getValue(), values);
103         return values.toArray(new String[values.size()]); 
104       }
105     }
106     if (currentType.equals(annotationTypeName)) {
107       //no field matched
108       return new String[]{};
109     }
110     
111     for (AnnotationMirror a : am.getAnnotationType().getAnnotationMirrors()) {
112       //cachable here...
113       if (!a.getAnnotationType().asElement().toString().startsWith("java.lang.annotation")) {
114         String[] output = getAnnotationValue(a, annotationTypeName, methodName);
115         if (output != null) {
116           return output;
117         }
118       }
119     }
120     return null;
121   }
122   
123   
124   /**
125    * On an executable element (that is a value holder on annotation) extract any direct uses of @AlaisFor.
126    * 
127    * @param annotationParameter the annotation's parameter to inspect for uses of @AliasFor
128    * @return an AliasData if the the annotation is found, null otherwise.
129    */
130   private static AliasData getAlias(ExecutableElement annotationParameter) {
131     AliasData output = null;
132     for (AnnotationMirror am : annotationParameter.getAnnotationMirrors()) {
133       if (ALIAS_TYPE.equals(am.getAnnotationType().asElement().toString())) {
134         output = new AliasData();
135         for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
136           String fieldName = ev.getKey().getSimpleName().toString();
137           if (ALIAS_TARGET_TYPE.equals(fieldName)) {
138             if (ev.getValue() != null && ev.getValue().getValue() != null) {
139               output.targetAnnotation = ev.getValue().getValue().toString();
140             } else {
141               return null;
142             }
143           }
144           if (ALIAS_TARGET_FIELD.equals(fieldName) 
145               && ev.getValue() != null && ev.getValue().getValue() != null) {
146             output.targetField = ev.getValue().getValue().toString();
147           }
148           if (DEFAULT_ANNOTATION_VALUE.equals(fieldName)
149               && (ev.getValue() != null && ev.getValue().getValue() != null)) {
150             output.targetField = ev.getValue().getValue().toString();
151           }
152         }
153       }
154     }
155     return output;
156   }
157   
158   /**
159    *  Checks to see if the aliasData matches the targetType and targetField.   The aliasData may have a null
160    *  targetType and if so, the currentAnnotation is used to determine if the targetType Matches.
161    *  This indicates that the AliasFor annotation is on an element in the targetType annotation itself.
162    */
163   private static boolean aliasMatch(AliasData aliasData, String targetType, String targetField, String currentAnnotation) {
164     if (aliasData == null) {
165       return false;
166     }
167     return (//types match
168         (targetType.equals(aliasData.targetAnnotation)
169         || (aliasData.targetAnnotation == null && targetType.equals(currentAnnotation)))
170         && //fields match
171         targetField.equals(aliasData.targetField));
172   }
173   
174   private static class AnnotationValueExtractorVisitor extends SimpleAnnotationValueVisitor8<Void, List<String>> {
175 
176     @Override
177     protected Void defaultAction(Object o, List<String> values) {
178       values.add(o.toString());
179       return null;
180     }
181 
182     public Void visitEnumConstant(VariableElement c, List<String> values) {
183       values.add(c.getSimpleName().toString());
184       return null;
185     }
186 
187     public Void visitAnnotation(AnnotationMirror a, List<String> values) {
188       // should probably do something here, but what? return annotation types?
189       return defaultAction(a, values);
190     }
191 
192     public Void visitArray(List<? extends AnnotationValue> vals, List<String> values) {
193       for (AnnotationValue val : vals) {
194         visit(val, values);
195       }
196       return null;
197     }
198   }
199 }