View Javadoc
1   /*
2    * Copyright © 2017, Saleforce.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 @Verifed annotation.
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
126    * direct uses of @AlaisFor
127    * 
128    * @param annotationParameter the annotation's parameter to inspect for uses of @AliasFor
129    * @return an AliasData if the the annotation is found, null otherwise.
130    */
131   private static AliasData getAlias(ExecutableElement annotationParameter) {
132     AliasData output = null;
133     for (AnnotationMirror am : annotationParameter.getAnnotationMirrors()) {
134       if (ALIAS_TYPE.equals(am.getAnnotationType().asElement().toString())) {
135         output = new AliasData();
136         for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
137           String fieldName = ev.getKey().getSimpleName().toString();
138           if (ALIAS_TARGET_TYPE.equals(fieldName)) {
139             if (ev.getValue() != null && ev.getValue().getValue() != null) {
140               output.targetAnnotation = ev.getValue().getValue().toString();
141             } else {
142               return null;
143             }
144           }
145           if (ALIAS_TARGET_FIELD.equals(fieldName) 
146               && ev.getValue() != null && ev.getValue().getValue() != null) {
147             output.targetField = ev.getValue().getValue().toString();
148           }
149           if (DEFAULT_ANNOTATION_VALUE.equals(fieldName)
150               && (ev.getValue() != null && ev.getValue().getValue() != null)) {
151             output.targetField = ev.getValue().getValue().toString();
152           }
153         }
154       }
155     }
156     return output;
157   }
158   
159   /**
160    *  Checks to see if the aliasData matches the targetType and targetField.   The aliasData may have a null
161    *  targetType and if so, the currentAnnotation is used to determine if the targetType Matches.
162    *  This indicates that the AliasFor annotation is on an element in the targetType annotation itself.
163    */
164   private static boolean aliasMatch(AliasData aliasData, String targetType, String targetField, String currentAnnotation) {
165     if (aliasData == null) {
166       return false;
167     }
168     return (//types match
169         (targetType.equals(aliasData.targetAnnotation)
170         || (aliasData.targetAnnotation == null && targetType.equals(currentAnnotation)))
171         && //fields match
172         targetField.equals(aliasData.targetField));
173   }
174   
175   private static class AnnotationValueExtractorVisitor extends SimpleAnnotationValueVisitor8<Void, List<String>> {
176 
177     @Override
178     protected Void defaultAction(Object o, List<String> values) {
179       values.add(o.toString());
180       return null;
181     }
182 
183     public Void visitEnumConstant(VariableElement c, List<String> values) {
184       values.add(c.getSimpleName().toString());
185       return null;
186     }
187 
188     public Void visitAnnotation(AnnotationMirror a, List<String> values) {
189       // should probably do something here, but what? return annotation types?
190       return defaultAction(a, values);
191     }
192 
193     public Void visitArray(List<? extends AnnotationValue> vals, List<String> values) {
194       for (AnnotationValue val : vals) {
195         visit(val, values);
196       }
197       return null;
198     }
199   }
200 }