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, meaning that an alias of another field will work, but not an alias of an alias.   TODO: This could be corrected 
87     * with an algorithm for extracting all the values off of Annotations in to another, cached, data set, but doesn't currently
88     * appear to be needed for standard spring annotations - user annotations may trip up on this.
89     *
90     * @param am the annotation to parse for a value.
91     * @param annotationTypeName the type of the annotation we are interested in, necessary for meta-annotation processing.
92     * @param methodName the name of the parameter designating the value 
93     * @return if the annotation or meta annotation is found, the AnnotationValues are converted to strings by 
94     *    {@link AnnotationValueExtractor} and returned in an array.  
95     */
96    private static String[] getAnnotationValue(AnnotationMirror am, String annotationTypeName, String methodName) {
97      String currentType = am.getAnnotationType().toString();
98      for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
99        boolean aliasMatch = aliasMatch(getAlias(ev.getKey()), annotationTypeName, methodName, currentType);
100       boolean foundField = ev.getKey().getSimpleName().toString().equals(methodName);
101       if (aliasMatch || (foundField && currentType.equals(annotationTypeName))) {
102         AnnotationValueExtractorVisitor ex = new AnnotationValueExtractorVisitor();
103         List<String> values = new ArrayList<>();
104         ex.visit(ev.getValue(), values);
105         return values.toArray(new String[values.size()]); 
106       }
107     }
108     if (currentType.equals(annotationTypeName)) {
109       //no field matched
110       return new String[]{};
111     }
112     
113     for (AnnotationMirror a : am.getAnnotationType().getAnnotationMirrors()) {
114       //cachable here...
115       if (!a.getAnnotationType().asElement().toString().startsWith("java.lang.annotation")) {
116         String[] output = getAnnotationValue(a, annotationTypeName, methodName);
117         if (output != null) {
118           return output;
119         }
120       }
121     }
122     return null;
123   }
124 
125   /**
126    * On an executable element (that is a value holder on annotation) extract any direct uses of @AlaisFor. 
127    * Meaning that an alias of another field will work, but not an alias of an alias.   TODO: This could be corrected 
128    * with an algorithm for extracting all the values off of Annotations in to another, cached, data set, but doesn't currently
129    * appear to be needed for standard spring annotations - user annotations may trip up on this.
130    * 
131    * @param annotationParameter the annotation's parameter to inspect for uses of @AliasFor
132    * @return an AliasData if the the annotation is found, null otherwise.
133    */
134   private static AliasData getAlias(ExecutableElement annotationParameter) {
135     AliasData output = null;
136     for (AnnotationMirror am : annotationParameter.getAnnotationMirrors()) {
137       if (ALIAS_TYPE.equals(am.getAnnotationType().asElement().toString())) {
138         output = new AliasData();
139         for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : am.getElementValues().entrySet()) {
140           String fieldName = ev.getKey().getSimpleName().toString();
141           AnnotationValue av = ev.getValue();
142           if (ALIAS_TARGET_TYPE.equals(fieldName)) {
143             output.targetAnnotation = getAttributeValueFromAnnotationFieldAsString(av, false);
144           }
145           //AliasFor has both "value" and "attribute" to specify the target
146           //annotation field, however, neither may be set which means that the
147           //the same name as the current annotation's field which is annotated
148           //with AliasFor will be used.
149           if (ALIAS_TARGET_FIELD.equals(fieldName)) {
150             output.targetField = getAttributeValueFromAnnotationFieldAsString(av, true);
151           }
152           if (DEFAULT_ANNOTATION_VALUE.equals(fieldName) && output.targetField == null) {
153             output.targetField = getAttributeValueFromAnnotationFieldAsString(av, true);
154           }
155         }
156         //if the fieldName isn't declared by either "value" or "attribute"
157         //assume that the existing field's name is it's value.
158         //hmm.... not good enough this may be a meta-annotation for a canonical annotation.
159         if (output.targetAnnotation != null && output.targetField == null) {
160           output.targetField = annotationParameter.getSimpleName().toString();
161         }
162       }
163     }
164     return output;
165   }
166   
167   /**
168    * Given an annotationValue (a field on an Annotation) extract the string representation of
169    * it's single value (not for nested annotations as a value in an annotation).
170    * 
171    * @param av The annotation value to extract
172    * @param emptyStringAsNull if the return value would be an empty string instead return null;
173    * @return the string representation of the annotation or null;
174    */
175   private static String getAttributeValueFromAnnotationFieldAsString(AnnotationValue av, boolean emptyStringAsNull) {
176     if (av != null && av.getValue() != null) {
177       return av.getValue().toString().isEmpty() && emptyStringAsNull ? null : av.getValue().toString();
178     } else { 
179       return null;
180     }
181   }
182   
183   /**
184    *  Checks to see if the aliasData matches the targetType and targetField.   The aliasData may have a null
185    *  targetType and if so, the currentAnnotation is used to determine if the targetType Matches.
186    *  This indicates that the AliasFor annotation is on an element in the targetType annotation itself.
187    */
188   private static boolean aliasMatch(AliasData aliasData, String targetType, String targetField, String currentAnnotation) {
189     if (aliasData == null) {
190       return false;
191     }
192     return (//types match
193         (targetType.equals(aliasData.targetAnnotation)
194         || (aliasData.targetAnnotation == null && targetType.equals(currentAnnotation)))
195         && //fields match
196         targetField.equals(aliasData.targetField));
197   }
198   
199   private static class AnnotationValueExtractorVisitor extends SimpleAnnotationValueVisitor8<Void, List<String>> {
200 
201     @Override
202     protected Void defaultAction(Object o, List<String> values) {
203       values.add(o.toString());
204       return null;
205     }
206 
207     public Void visitEnumConstant(VariableElement c, List<String> values) {
208       values.add(c.getSimpleName().toString());
209       return null;
210     }
211 
212     public Void visitAnnotation(AnnotationMirror a, List<String> values) {
213       // should probably do something here, but what? return annotation types?
214       return defaultAction(a, values);
215     }
216 
217     public Void visitArray(List<? extends AnnotationValue> vals, List<String> values) {
218       for (AnnotationValue val : vals) {
219         visit(val, values);
220       }
221       return null;
222     }
223   }
224 }