1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.processor.view.inspector;
18 
19 import androidx.annotation.NonNull;
20 
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.stream.Collectors;
27 
28 import javax.annotation.processing.ProcessingEnvironment;
29 import javax.lang.model.element.AnnotationMirror;
30 import javax.lang.model.element.AnnotationValue;
31 import javax.lang.model.element.Element;
32 import javax.lang.model.element.ExecutableElement;
33 import javax.lang.model.element.TypeElement;
34 import javax.lang.model.type.TypeMirror;
35 import javax.lang.model.util.Elements;
36 import javax.lang.model.util.Types;
37 
38 /**
39  * Utilities for working with {@link AnnotationMirror}.
40  */
41 final class AnnotationUtils {
42     private final Elements mElementUtils;
43     private final Types mTypeUtils;
44 
AnnotationUtils(@onNull ProcessingEnvironment processingEnv)45     AnnotationUtils(@NonNull ProcessingEnvironment processingEnv) {
46         mElementUtils = processingEnv.getElementUtils();
47         mTypeUtils = processingEnv.getTypeUtils();
48     }
49 
50     /**
51      * Get a {@link AnnotationMirror} specified by name from an {@link Element}.
52      *
53      * @param qualifiedName The fully qualified name of the annotation to search for
54      * @param element The element to search for annotations on
55      * @return The mirror of the requested annotation
56      * @throws ProcessingException If there is not exactly one of the requested annotation.
57      */
58     @NonNull
exactlyOneMirror(@onNull String qualifiedName, @NonNull Element element)59     AnnotationMirror exactlyOneMirror(@NonNull String qualifiedName, @NonNull Element element) {
60         final Element targetTypeElment = mElementUtils.getTypeElement(qualifiedName);
61         final TypeMirror targetType = targetTypeElment.asType();
62         AnnotationMirror result = null;
63 
64         for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
65             final TypeMirror annotationType = annotation.getAnnotationType().asElement().asType();
66             if (mTypeUtils.isSameType(annotationType, targetType)) {
67                 if (result == null) {
68                     result = annotation;
69                 } else {
70                     final String message = String.format(
71                             "Element had multiple instances of @%s, expected exactly one",
72                             targetTypeElment.getSimpleName());
73 
74                     throw new ProcessingException(message, element, annotation);
75                 }
76             }
77         }
78 
79         if (result == null) {
80             final String message = String.format(
81                     "Expected an @%s annotation, found none", targetTypeElment.getSimpleName());
82             throw new ProcessingException(message, element);
83         } else {
84             return result;
85         }
86     }
87 
88     /**
89      * Determine if an annotation with the supplied qualified name is present on the element.
90      *
91      * @param element The element to check for the presence of an annotation
92      * @param annotationQualifiedName The name of the annotation to check for
93      * @return True if the annotation is present, false otherwise
94      */
hasAnnotation(@onNull Element element, @NonNull String annotationQualifiedName)95     boolean hasAnnotation(@NonNull Element element, @NonNull String annotationQualifiedName) {
96         final TypeElement namedElement = mElementUtils.getTypeElement(annotationQualifiedName);
97 
98         if (namedElement != null) {
99             final TypeMirror annotationType = namedElement.asType();
100 
101             for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
102                 if (mTypeUtils.isSubtype(annotation.getAnnotationType(), annotationType)) {
103                     return true;
104                 }
105             }
106         }
107 
108         return false;
109     }
110 
111     /**
112      * Get a typed list of values for an annotation array property by name.
113      *
114      * The returned list will be empty if the value was left at the default.
115      *
116      * @param propertyName The name of the property to search for
117      * @param valueClass The expected class of the property value
118      * @param element The element the annotation is on, used for exceptions
119      * @param annotationMirror An annotation mirror to search for the property
120      * @param <T> The type of the value
121      * @return A list containing the requested types
122      */
typedArrayValuesByName( @onNull String propertyName, @NonNull Class<T> valueClass, @NonNull Element element, @NonNull AnnotationMirror annotationMirror)123     <T> List<T> typedArrayValuesByName(
124             @NonNull String propertyName,
125             @NonNull Class<T> valueClass,
126             @NonNull Element element,
127             @NonNull AnnotationMirror annotationMirror) {
128         return untypedArrayValuesByName(propertyName, element, annotationMirror)
129                 .stream()
130                 .map(annotationValue -> {
131                     final Object value = annotationValue.getValue();
132 
133                     if (value == null) {
134                         throw new ProcessingException(
135                                 "Unexpected null in array.",
136                                 element,
137                                 annotationMirror,
138                                 annotationValue);
139                     }
140 
141                     if (valueClass.isAssignableFrom(value.getClass())) {
142                         return valueClass.cast(value);
143                     } else {
144                         throw new ProcessingException(
145                                 String.format(
146                                         "Expected array entry to have type %s, but got %s.",
147                                         valueClass.getCanonicalName(),
148                                         value.getClass().getCanonicalName()),
149                                 element,
150                                 annotationMirror,
151                                 annotationValue);
152                     }
153                 })
154                 .collect(Collectors.toList());
155     }
156 
157     /**
158      * Get a list of values for an annotation array property by name.
159      *
160      * @param propertyName The name of the property to search for
161      * @param element The element the annotation is on, used for exceptions
162      * @param annotationMirror An annotation mirror to search for the property
163      * @return A list of annotation values, empty list if none found
164      */
165     @NonNull
untypedArrayValuesByName( @onNull String propertyName, @NonNull Element element, @NonNull AnnotationMirror annotationMirror)166     List<AnnotationValue> untypedArrayValuesByName(
167             @NonNull String propertyName,
168             @NonNull Element element,
169             @NonNull AnnotationMirror annotationMirror) {
170         return typedValueByName(propertyName, List.class, element, annotationMirror)
171                 .map(untypedValues -> {
172                     List<AnnotationValue> typedValues = new ArrayList<>(untypedValues.size());
173 
174                     for (Object untypedValue : untypedValues) {
175                         if (untypedValue instanceof AnnotationValue) {
176                             typedValues.add((AnnotationValue) untypedValue);
177                         } else {
178                             throw new ProcessingException(
179                                     "Unable to convert array entry to AnnotationValue",
180                                     element,
181                                     annotationMirror);
182                         }
183                     }
184 
185                     return typedValues;
186                 }).orElseGet(Collections::emptyList);
187     }
188 
189     /**
190      * Get the typed value of an annotation property by name.
191      *
192      * The returned optional will be empty if the value was left at the default, or if the value
193      * of the property is null.
194      *
195      * @param propertyName The name of the property to search for
196      * @param valueClass The expected class of the property value
197      * @param element The element the annotation is on, used for exceptions
198      * @param annotationMirror An annotation mirror to search for the property
199      * @param <T> The type of the value
200      * @return An optional containing the typed value of the named property
201      */
202     @NonNull
203     <T> Optional<T> typedValueByName(
204             String propertyName,
205             Class<T> valueClass,
206             Element element,
207             AnnotationMirror annotationMirror) {
208         return valueByName(propertyName, annotationMirror).map(annotationValue -> {
209             final Object value = annotationValue.getValue();
210 
211             if (value == null) {
212                 throw new ProcessingException(
213                         String.format(
214                                 "Unexpected null value for annotation property \"%s\".",
215                                 propertyName),
216                         element,
217                         annotationMirror,
218                         annotationValue);
219             }
220 
221             if (valueClass.isAssignableFrom(value.getClass())) {
222                 return valueClass.cast(value);
223             } else {
224                 throw new ProcessingException(
225                         String.format(
226                                 "Expected annotation property \"%s\" to have type %s, but got %s.",
227                                 propertyName,
228                                 valueClass.getCanonicalName(),
229                                 value.getClass().getCanonicalName()),
230                         element,
231                         annotationMirror,
232                         annotationValue);
233             }
234         });
235     }
236 
237     /**
238      * Get the untyped value of an annotation property by name.
239      *
240      * The returned optional will be empty if the value was left at the default, or if the value
241      * of the property is null.
242      *
243      * @param propertyName The name of the property to search for
244      * @param element The element the annotation is on, used for exceptions
245      * @param annotationMirror An annotation mirror to search for the property
246      * @return An optional containing the untyped value of the named property
247      * @see AnnotationValue#getValue()
248      */
249     @NonNull
250     Optional<Object> untypedValueByName(
251             @NonNull String propertyName,
252             @NonNull Element element,
253             @NonNull AnnotationMirror annotationMirror) {
254         return valueByName(propertyName, annotationMirror).map(annotationValue -> {
255             final Object value = annotationValue.getValue();
256 
257             if (value == null) {
258                 throw new ProcessingException(
259                         String.format(
260                                 "Unexpected null value for annotation property \"%s\".",
261                                 propertyName),
262                         element,
263                         annotationMirror,
264                         annotationValue);
265             }
266 
267             return value;
268         });
269     }
270 
271     /**
272      * Extract a {@link AnnotationValue} from a mirror by string property name.
273      *
274      * @param propertyName The name of the property requested property
275      * @param annotationMirror The mirror to search for the property
276      * @return The value of the property
277      */
278     @NonNull
279     Optional<AnnotationValue> valueByName(
280             @NonNull String propertyName,
281             @NonNull AnnotationMirror annotationMirror) {
282         final Map<? extends ExecutableElement, ? extends AnnotationValue> valueMap =
283                 annotationMirror.getElementValues();
284 
285         for (ExecutableElement method : valueMap.keySet()) {
286             if (method.getSimpleName().contentEquals(propertyName)) {
287                 return Optional.ofNullable(valueMap.get(method));
288             }
289         }
290 
291         // Property not explicitly defined, use default value.
292         return Optional.empty();
293     }
294 }
295