All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.f2prateek.dart.common.AbstractDartProcessor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2013 Jake Wharton
 * Copyright 2014 Prateek Srivastava (@f2prateek)
 *
 * 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 com.f2prateek.dart.common;

import com.f2prateek.dart.InjectExtra;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.tools.Diagnostic.Kind.ERROR;

/**
 * Base class of the annotation processors of Dart.
 * It collects all information about extra injections to be performed
 * when scanning the {@link com.f2prateek.dart.InjectExtra} annotations of given source files.
 * The collected information is stored in a collection of {@code InjectionTarget}.
 * All annotations processors in Dart support the option {@code #OPTION_DART_DEBUG}
 * that will log information about annotation processor and generated code.
 *
 * @see #findAndParseTargets(RoundEnvironment)
 */
public abstract class AbstractDartProcessor extends AbstractProcessor {

  public static final String OPTION_DART_DEBUG = "dart.debug";

  // @formatter:off
  //You must go to Preferences->Code Style->General->Formatter Control
  // and check Enable formatter markers in comments for this to work.
  private static final Set JAVA_KEYWORDS = new HashSet<>(Arrays.asList(
      "abstract",  "assert",       "boolean",    "break",      "byte",      "case",
      "catch",     "char",         "class",      "const",     "continue",   "enum",
      "default",   "do",           "double",     "else",      "extends",    "while",
      "false",     "final",        "finally",    "float",     "for",
      "goto",      "if",           "implements", "import",    "instanceof",
      "int",       "interface",    "long",       "native",    "new",
      "null",      "package",      "private",    "protected", "public",
      "return",    "short",        "static",     "strictfp",  "super",
      "switch",    "synchronized", "this",       "throw",     "throws",
      "transient", "true",         "try",        "void",      "volatile"
  ));
  // @formatter:on

  private List singleCollections;
  private List doubleCollections;
  private Elements elementUtils;
  protected Types typeUtils;
  protected Filer filer;
  protected boolean isDebugEnabled;
  protected boolean usesParcelerOption = true;

  protected abstract Map findAndParseTargets(RoundEnvironment env);

  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();

    //note for maintenance : here we use class names directly
    //as a general rule of thumb, we should not use classes in an annotation processor
    //as those classes are different from classes seen at runtime.
    //Using class created a bug in apps built by gradle as SparseArray was not found during
    //annotation processing time.
    singleCollections = getTypeElements(new String[] {
        "java.util.List", "java.util.ArrayList", "java.util.LinkedList", "java.util.Set",
        "java.util.HashSet", "java.util.SortedSet", "java.util.TreeSet", "java.util.LinkedHashSet",
        "android.util.SparseArray"
    });

    doubleCollections = getTypeElements(new String[] {
        "java.util.Map", "java.util.HashMap", "java.util.LinkedHashMap", "java.util.SortedMap",
        "java.util.TreeMap"
    });

    final Map options = env.getOptions();
    isDebugEnabled |= options.containsKey(OPTION_DART_DEBUG) && Boolean.parseBoolean(
        options.get(OPTION_DART_DEBUG));
  }

  @Override public Set getSupportedAnnotationTypes() {
    Set supportTypes = new LinkedHashSet<>();
    supportTypes.add(InjectExtra.class.getCanonicalName());
    return supportTypes;
  }

  @Override public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }

  @Override public Set getSupportedOptions() {
    Set supportedOptions = new LinkedHashSet<>();
    supportedOptions.add(OPTION_DART_DEBUG);
    return supportedOptions;
  }

  @SuppressWarnings("This method is only used when debugging/creating tests.")
  public void setIsDebugEnabled(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  @SuppressWarnings("This method is only used for testing.")
  public boolean isDebugEnabled() {
    return isDebugEnabled;
  }

  public void setUsesParcelerOption(boolean usesParcelerOption) {
    this.usesParcelerOption = usesParcelerOption;
  }

  protected InjectionTarget getOrCreateTargetClass(Map targetClassMap,
      TypeElement typeElement) {
    InjectionTarget injectionTarget = targetClassMap.get(typeElement);
    if (injectionTarget == null) {
      final String targetType = typeElement.getQualifiedName().toString();
      final String classPackage = getPackageName(typeElement);
      final String className = getClassName(typeElement, classPackage);
      final boolean isAbstractType = typeElement.getModifiers().contains(Modifier.ABSTRACT);

      injectionTarget = new InjectionTarget(classPackage, className, targetType, isAbstractType);
      targetClassMap.put(typeElement, injectionTarget);
    }
    return injectionTarget;
  }

  /** Finds the parent injector type in the supplied set, if any. */
  protected String findParentFqcn(TypeElement typeElement, Set parents) {
    TypeMirror type;
    while (true) {
      type = typeElement.getSuperclass();
      if (type.getKind() == TypeKind.NONE) {
        return null;
      }
      typeElement = (TypeElement) ((DeclaredType) type).asElement();
      if (containsTypeMirror(parents, type)) {
        String packageName = getPackageName(typeElement);
        return packageName + "." + getClassName(typeElement, packageName);
      }
    }
  }

  protected void error(Element element, String message, Object... args) {
    processingEnv.getMessager().printMessage(ERROR, String.format(message, args), element);
  }

  protected void parseInjectExtraAnnotatedElements(RoundEnvironment env,
      Map targetClassMap, Set erasedTargetTypes) {
    for (Element element : env.getElementsAnnotatedWith(InjectExtra.class)) {
      try {
        parseInjectExtra(element, targetClassMap, erasedTargetTypes);
      } catch (Exception e) {
        StringWriter stackTrace = new StringWriter();
        e.printStackTrace(new PrintWriter(stackTrace));

        error(element, "Unable to generate extra injector when parsing @InjectExtra.\n\n%s",
            stackTrace.toString());
      }
    }
  }

  private void parseInjectExtra(Element element, Map targetClassMap,
      Set erasedTargetTypes) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify common generated code restrictions.
    if (!isValidUsageOfInjectExtra(InjectExtra.class, element)) {
      return;
    }

    String annotationValue = element.getAnnotation(InjectExtra.class).value();
    if (!isNullOrEmpty(annotationValue) && !isValidJavaIdentifier(annotationValue)) {
      throw new IllegalArgumentException("Keys have to be valid java variable identifiers. "
          + "https://docs.oracle.com/cd/E19798-01/821-1841/bnbuk/index.html");
    }

    // Assemble information on the injection point.
    String name = element.getSimpleName().toString();
    String key = isNullOrEmpty(annotationValue) ? name : annotationValue;
    TypeMirror type = element.asType();
    boolean required = isRequiredInjection(element);
    boolean parcel = isParcelerAvailable() && isValidExtraTypeForParceler(type);

    InjectionTarget injectionTarget = getOrCreateTargetClass(targetClassMap, enclosingElement);
    injectionTarget.addField(key, name, type, required, parcel);

    // Add the type-erased version to the valid injection targets set.
    TypeMirror erasedTargetType = typeUtils.erasure(enclosingElement.asType());
    erasedTargetTypes.add(erasedTargetType);
  }

  private boolean isValidUsageOfInjectExtra(Class annotationClass,
      Element element) {
    boolean valid = true;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify modifiers.
    Set modifiers = element.getModifiers();
    if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
      error(element, "@%s fields must not be private or static. (%s.%s)",
          annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      valid = false;
    }

    //Verify that the type is primitive or serializable or parcelable
    TypeMirror typeElement = element.asType();
    if (!isValidExtraType(typeElement) && !(isParcelerAvailable() && isValidExtraTypeForParceler(
        typeElement))) {
      error(element, "@%s field must be a primitive or Serializable or Parcelable (%s.%s). "
              + "If you use Parceler, all types supported by Parceler are allowed.",
          annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      valid = false;
    }

    // Verify containing type.
    if (enclosingElement.getKind() != CLASS) {
      error(enclosingElement, "@%s fields may only be contained in classes. (%s.%s)",
          annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      valid = false;
    }

    // Verify containing class visibility is not private.
    if (enclosingElement.getModifiers().contains(PRIVATE)) {
      error(enclosingElement, "@%s fields may not be contained in private classes. (%s.%s)",
          annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      valid = false;
    }

    return valid;
  }

  private boolean isValidExtraType(TypeMirror type) {
    return isSerializable(type) || isParcelable(type) || isCharSequence(type);
  }

  private boolean isValidExtraTypeForParceler(TypeMirror type) {
    return isValidForParceler(type, false);
  }

  private boolean isValidForParceler(TypeMirror type, boolean subCollection) {
    if (subCollection && (isSerializable(type) || isParcelable(type))) {
      return true;
    }
    if (isAnnotatedWithParcel(type)) {
      return true;
    }
    if (type instanceof DeclaredType) {
      DeclaredType declaredType = (DeclaredType) type;
      if (existsWithin(type, singleCollections)) {
        return isValidForParceler(declaredType.getTypeArguments().get(0), true);
      }
      if (existsWithin(type, doubleCollections)) {
        return isValidForParceler(declaredType.getTypeArguments().get(0), true)
            && isValidForParceler(declaredType.getTypeArguments().get(1), true);
      }
    }
    return false;
  }

  private boolean isSerializable(TypeMirror type) {
    TypeMirror serializableTypeMirror =
        elementUtils.getTypeElement("java.io.Serializable").asType();
    return typeUtils.isAssignable(type, serializableTypeMirror);
  }

  private boolean isParcelable(TypeMirror type) {
    TypeMirror parcelableTypeMirror = elementUtils.getTypeElement("android.os.Parcelable").asType();
    return typeUtils.isAssignable(type, parcelableTypeMirror);
  }

  private boolean isCharSequence(TypeMirror type) {
    TypeMirror charSequenceTypeMirror =
        elementUtils.getTypeElement("java.lang.CharSequence").asType();
    return typeUtils.isAssignable(type, charSequenceTypeMirror);
  }

  private boolean isParcelerAvailable() {
    return usesParcelerOption && elementUtils.getTypeElement("org.parceler.Parcel") != null;
  }

  private boolean isAnnotatedWithParcel(TypeMirror type) {
    return hasAnnotationWithFqcn(typeUtils.asElement(type), "org.parceler.Parcel");
  }

  private boolean existsWithin(TypeMirror type, List supportedTypes) {
    for (Element supportedType : supportedTypes) {
      if (typeUtils.erasure(type).equals(typeUtils.erasure(supportedType.asType()))) {
        return true;
      }
    }
    return false;
  }

  private List getTypeElements(String[] classNames) {
    List elements = new ArrayList<>();
    for (String className : classNames) {
      elements.add(elementUtils.getTypeElement(className));
    }
    return elements;
  }

  /**
   * Returns {@code true} if the an annotation is found on the given element with the given class
   * name (not fully qualified).
   */
  private static boolean hasAnnotationWithName(Element element, String simpleName) {
    for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
      final Element annnotationElement = mirror.getAnnotationType().asElement();
      String annotationName = annnotationElement.getSimpleName().toString();
      if (simpleName.equals(annotationName)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns {@code true} if an injection is deemed to be required. Returns false when a field is
   * annotated with any annotation named {@code Optional} or {@code Nullable}.
   */
  private static boolean isRequiredInjection(Element element) {
    return !hasAnnotationWithName(element, "Nullable") && !hasAnnotationWithName(element,
        "Optional");
  }

  /**
   * Returns {@code true} if the an annotation is found on the given element with the given class
   * name (must be a fully qualified class name).
   */
  private static boolean hasAnnotationWithFqcn(Element element, String annotationClassNameName) {
    if (element != null) {
      for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
        if (annotationMirror.getAnnotationType()
            .asElement()
            .toString()
            .equals(annotationClassNameName)) {
          return true;
        }
      }
    }
    return false;
  }

  private static String getClassName(TypeElement type, String packageName) {
    int packageLen = packageName.length() + 1;
    return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
  }

  /**
   * Returns true if the string is null or 0-length.
   *
   * @param str the string to be examined
   * @return true if str is null or zero length
   */
  private static boolean isNullOrEmpty(String str) {
    return str == null || str.trim().length() == 0;
  }

  private boolean containsTypeMirror(Collection mirrors, TypeMirror query) {
    // Ensure we are checking against a type-erased version for normalization purposes.
    query = typeUtils.erasure(query);

    for (TypeMirror mirror : mirrors) {
      if (typeUtils.isSameType(mirror, query)) {
        return true;
      }
    }
    return false;
  }

  private String getPackageName(TypeElement type) {
    return elementUtils.getPackageOf(type).getQualifiedName().toString();
  }

  /**
   * Returns true if the string is a valid Java identifier.
   * See Identifiers
   *
   * @param str the string to be examined
   * @return true if str is a valid Java identifier
   */
  static boolean isValidJavaIdentifier(String str) {
    if (isNullOrEmpty(str)) {
      return false;
    }
    if (JAVA_KEYWORDS.contains(str)) {
      return false;
    }
    if (!Character.isJavaIdentifierStart(str.charAt(0))) {
      return false;
    }
    for (int i = 1; i < str.length(); i++) {
      if (!Character.isJavaIdentifierPart(str.charAt(i))) {
        return false;
      }
    }
    return true;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy