content: /*
   * Copyright 2019 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.processor.view.inspector;
  
  import androidx.annotation.NonNull;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.stream.Collectors;
  
  import javax.annotation.processing.ProcessingEnvironment;
  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.type.TypeMirror;
  import javax.lang.model.util.Elements;
  import javax.lang.model.util.Types;
  
  /**
   * Utilities for working with {@link AnnotationMirror}.
   */
  final class AnnotationUtils {
      private final Elements mElementUtils;
      private final Types mTypeUtils;
  
      AnnotationUtils(@NonNull ProcessingEnvironment processingEnv) {
          mElementUtils = processingEnv.getElementUtils();
          mTypeUtils = processingEnv.getTypeUtils();
      }
  
      /**
       * Get a {@link AnnotationMirror} specified by name from an {@link Element}.
       *
       * @param qualifiedName The fully qualified name of the annotation to search for
       * @param element The element to search for annotations on
       * @return The mirror of the requested annotation
       * @throws ProcessingException If there is not exactly one of the requested annotation.
       */
      @NonNull
      AnnotationMirror exactlyOneMirror(@NonNull String qualifiedName, @NonNull Element element) {
          final Element targetTypeElment = mElementUtils.getTypeElement(qualifiedName);
          final TypeMirror targetType = targetTypeElment.asType();
          AnnotationMirror result = null;
  
          for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
              final TypeMirror annotationType = annotation.getAnnotationType().asElement().asType();
              if (mTypeUtils.isSameType(annotationType, targetType)) {
                  if (result == null) {
                      result = annotation;
                  } else {
                      final String message = String.format(
                              "Element had multiple instances of @%s, expected exactly one",
                              targetTypeElment.getSimpleName());
  
                      throw new ProcessingException(message, element, annotation);
                  }
              }
          }
  
          if (result == null) {
              final String message = String.format(
                      "Expected an @%s annotation, found none", targetTypeElment.getSimpleName());
              throw new ProcessingException(message, element);
          } else {
              return result;
          }
      }
  
      /**
       * Determine if an annotation with the supplied qualified name is present on the element.
       *
       * @param element The element to check for the presence of an annotation
       * @param annotationQualifiedName The name of the annotation to check for
       * @return True if the annotation is present, false otherwise
       */
      boolean hasAnnotation(@NonNull Element element, @NonNull String annotationQualifiedName) {
          final TypeElement namedElement = mElementUtils.getTypeElement(annotationQualifiedName);
  
          if (namedElement != null) {
              final TypeMirror annotationType = namedElement.asType();
  
              for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
                  if (mTypeUtils.isSubtype(annotation.getAnnotationType(), annotationType)) {
                      return true;
                  }
              }
          }
  
          return false;
      }
  
      /**
       * Get a typed list of values for an annotation array property by name.
       *
       * The returned list will be empty if the value was left at the default.
       *
       * @param propertyName The name of the property to search for
       * @param valueClass The expected class of the property value
       * @param element The element the annotation is on, used for exceptions
       * @param annotationMirror An annotation mirror to search for the property
       * @param <T> The type of the value
       * @return A list containing the requested types
       */
      <T> List<T> typedArrayValuesByName(
              @NonNull String propertyName,
              @NonNull Class<T> valueClass,
              @NonNull Element element,
              @NonNull AnnotationMirror annotationMirror) {
          return untypedArrayValuesByName(propertyName, element, annotationMirror)
                  .stream()
                  .map(annotationValue -> {
                      final Object value = annotationValue.getValue();
  
                      if (value == null) {
                          throw new ProcessingException(
                                  "Unexpected null in array.",
                                  element,
                                  annotationMirror,
                                  annotationValue);
                      }
  
                      if (valueClass.isAssignableFrom(value.getClass())) {
                          return valueClass.cast(value);
                      } else {
                          throw new ProcessingException(
                                  String.format(
                                          "Expected array entry to have type %s, but got %s.",
                                          valueClass.getCanonicalName(),
                                          value.getClass().getCanonicalName()),
                                  element,
                                  annotationMirror,
                                  annotationValue);
                      }
                  })
                  .collect(Collectors.toList());
      }
  
      /**
       * Get a list of values for an annotation array property by name.
       *
       * @param propertyName The name of the property to search for
       * @param element The element the annotation is on, used for exceptions
       * @param annotationMirror An annotation mirror to search for the property
       * @return A list of annotation values, empty list if none found
       */
      @NonNull
      List<AnnotationValue> untypedArrayValuesByName(
              @NonNull String propertyName,
              @NonNull Element element,
              @NonNull AnnotationMirror annotationMirror) {
          return typedValueByName(propertyName, List.class, element, annotationMirror)
                  .map(untypedValues -> {
                      List<AnnotationValue> typedValues = new ArrayList<>(untypedValues.size());
  
                      for (Object untypedValue : untypedValues) {
                          if (untypedValue instanceof AnnotationValue) {
                              typedValues.add((AnnotationValue) untypedValue);
                          } else {
                              throw new ProcessingException(
                                      "Unable to convert array entry to AnnotationValue",
                                      element,
                                      annotationMirror);
                          }
                      }
  
                      return typedValues;
                  }).orElseGet(Collections::emptyList);
      }
  
      /**
       * Get the typed value of an annotation property by name.
       *
       * The returned optional will be empty if the value was left at the default, or if the value
       * of the property is null.
       *
       * @param propertyName The name of the property to search for
       * @param valueClass The expected class of the property value
       * @param element The element the annotation is on, used for exceptions
       * @param annotationMirror An annotation mirror to search for the property
       * @param <T> The type of the value
       * @return An optional containing the typed value of the named property
       */
      @NonNull
      <T> Optional<T> typedValueByName(
              String propertyName,
              Class<T> valueClass,
              Element element,
              AnnotationMirror annotationMirror) {
          return valueByName(propertyName, annotationMirror).map(annotationValue -> {
              final Object value = annotationValue.getValue();
  
              if (value == null) {
                  throw new ProcessingException(
                          String.format(
                                  "Unexpected null value for annotation property \"%s\".",
                                  propertyName),
                          element,
                          annotationMirror,
                          annotationValue);
              }
  
              if (valueClass.isAssignableFrom(value.getClass())) {
                  return valueClass.cast(value);
              } else {
                  throw new ProcessingException(
                          String.format(
                                  "Expected annotation property \"%s\" to have type %s, but got %s.",
                                  propertyName,
                                  valueClass.getCanonicalName(),
                                  value.getClass().getCanonicalName()),
                          element,
                          annotationMirror,
                          annotationValue);
              }
          });
      }
  
      /**
       * Get the untyped value of an annotation property by name.
       *
       * The returned optional will be empty if the value was left at the default, or if the value
       * of the property is null.
       *
       * @param propertyName The name of the property to search for
       * @param element The element the annotation is on, used for exceptions
       * @param annotationMirror An annotation mirror to search for the property
       * @return An optional containing the untyped value of the named property
       * @see AnnotationValue#getValue()
       */
      @NonNull
      Optional<Object> untypedValueByName(
              @NonNull String propertyName,
              @NonNull Element element,
              @NonNull AnnotationMirror annotationMirror) {
          return valueByName(propertyName, annotationMirror).map(annotationValue -> {
              final Object value = annotationValue.getValue();
  
              if (value == null) {
                  throw new ProcessingException(
                          String.format(
                                  "Unexpected null value for annotation property \"%s\".",
                                  propertyName),
                          element,
                          annotationMirror,
                          annotationValue);
              }
  
              return value;
          });
      }
  
      /**
       * Extract a {@link AnnotationValue} from a mirror by string property name.
       *
       * @param propertyName The name of the property requested property
       * @param annotationMirror The mirror to search for the property
       * @return The value of the property
       */
      @NonNull
      Optional<AnnotationValue> valueByName(
              @NonNull String propertyName,
              @NonNull AnnotationMirror annotationMirror) {
          final Map<? extends ExecutableElement, ? extends AnnotationValue> valueMap =
                  annotationMirror.getElementValues();
  
          for (ExecutableElement method : valueMap.keySet()) {
              if (method.getSimpleName().contentEquals(propertyName)) {
                  return Optional.ofNullable(valueMap.get(method));
              }
          }
  
          // Property not explicitly defined, use default value.
          return Optional.empty();
      }
  }
  