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

auto.parse.processor.AutoParseProcessor Maven / Gradle / Ivy

/*
 * Copyright (C) 2012 Google, Inc.
 *
 * 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 auto.parse.processor;

import com.google.auto.service.AutoService;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

import auto.parse.AutoParse;

/**
 * Javac annotation processor (compiler plugin) for value types; user code never references this
 * class.
 *
 * @see auto.parse.AutoParse
 * @author Éamonn McManus
 */
@AutoService(Processor.class)
@SupportedOptions(EclipseHack.ENABLING_OPTION)
public class AutoParseProcessor extends AbstractProcessor {
  private static final boolean SILENT = true;

  public AutoParseProcessor() {}

  @Override
  public Set getSupportedAnnotationTypes() {
    return Collections.singleton(AutoParse.class.getName());
  }

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

  private void note(String msg) {
    if (!SILENT) {
      processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
    }
  }

  @SuppressWarnings("serial")
  // CHECKSTYLE:OFF:WhitespaceAround
  private static class CompileException extends Exception {}
  // CHECKSTYLE:ON

  /**
   * Issue a compilation error. This method does not throw an exception, since we want to
   * continue processing and perhaps report other errors. It is a good idea to introduce a
   * test case in CompilationErrorsTest for any new call to reportError(...) to ensure that we
   * continue correctly after an error.
   */
  private void reportError(String msg, Element e) {
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, e);
  }

  /**
   * Issue a compilation error and abandon the processing of this class. This does not prevent
   * the processing of other classes.
   */
  private void abortWithError(String msg, Element e) throws CompileException {
    reportError(msg, e);
    throw new CompileException();
  }

  @Override
  public boolean process(Set annotations, RoundEnvironment roundEnv) {
    boolean claimed = (annotations.size() == 1
        && annotations.iterator().next().getQualifiedName().toString().equals(
            AutoParse.class.getName()));
    if (claimed) {
      process(roundEnv);
      return true;
    } else {
      return false;
    }
  }

  private void process(RoundEnvironment roundEnv) {
    Collection annotatedElements =
        roundEnv.getElementsAnnotatedWith(AutoParse.class);
    Collection types = ElementFilter.typesIn(annotatedElements);
    for (TypeElement type : types) {
      try {
        processType(type);
      } catch (CompileException e) {
        // We abandoned this type, but continue with the next.
      } catch (RuntimeException e) {
        // Don't propagate this exception, which will confusingly crash the compiler.
        reportError("@AutoParse processor threw an exception: " + e, type);
      }
    }
  }

  private String generatedClassName(TypeElement type, String prefix) {
    String name = type.getSimpleName().toString();
    while (type.getEnclosingElement() instanceof TypeElement) {
      type = (TypeElement) type.getEnclosingElement();
      name = type.getSimpleName() + "_" + name;
    }
    String pkg = TypeSimplifier.packageNameOf(type);
    String dot = pkg.isEmpty() ? "" : ".";
    return pkg + dot + prefix + name;
  }

  private String generatedSubclassName(TypeElement type) {
    return generatedClassName(type, "AutoParse_");
  }

  private static String simpleNameOf(String s) {
    if (s.contains(".")) {
      return s.substring(s.lastIndexOf('.') + 1);
    } else {
      return s;
    }
  }

  // Return the name of the class, including any enclosing classes but not the package.
  private static String classNameOf(TypeElement type) {
    String name = type.getQualifiedName().toString();
    String pkgName = TypeSimplifier.packageNameOf(type);
    if (!pkgName.isEmpty()) {
      return name.substring(pkgName.length() + 1);
    } else {
      return name;
    }
  }

  // This is just because I hate typing    "...\n" +   all the time.
  private static String concatLines(String... lines) {
    StringBuilder sb = new StringBuilder();
    for (String line : lines) {
      sb.append(line).append("\n");
    }
    return sb.toString();
  }

  // The code below uses a small templating language. This is not hugely readable, but is much more
  // so than sb.append(this).append(that) with ifs and fors scattered around everywhere.
  // See the Template class for an explanation of the various constructs.
  private static final String TEMPLATE_STRING = concatLines(
      // CHECKSTYLE:OFF:OperatorWrap
      // Package declaration
      "$[pkg?package $[pkg];\n]",

      // Imports
      "$[imports:i||import $[i];\n]",

      "import java.util.Date;",
      "import org.json.JSONArray;",
      "import org.json.JSONObject;",
      "import java.util.List;",
      "import java.util.Map;",
      "import com.parse.ParseFile;",
      "import com.parse.ParseGeoPoint;",
      "import com.parse.ParseObject;",
      "import com.parse.ParseRelation;",
      "import com.parse.ParseUser;\n",

      // Class declaration
      "public class $[subclass]$[formaltypes] extends $[origclass]$[actualtypes] {",

      // Fields
      //"$[props:p||  public $[p.type] $[p];\n]",

      "  ParseObject parseObject;\n",

      // Constructor
      "  public $[subclass]() {",
      "    this.parseObject = this;",
      "  }\n",

      "  public $[subclass](ParseObject parseObject) {",
      "    this.parseObject = parseObject;",
      "  }\n",

      "  public $[subclass](\n      $[props:p|,\n      |$[p.type] $[p]]) {",
      "    this();",
      "$[props:p|\n|",
      "    this.$[p] = $[p];]",
      "  }\n",

      "  public $[origclass]$[actualtypes] commit() {",
      "$[props:p|\n|",
      "    parseObject.put(\"$[p]\", $[p]);]",
      "    return ($[origclass]$[actualtypes]) this;",
      "  }\n",

      "  private Object _get(String key, Object defValue) {",
      "      return parseObject.get(key);",
      "  }\n",

      "  private boolean _get(String key, Boolean defValue) {",
      "      return parseObject.getBoolean(key);",
      "  }\n",

      "  private byte[] _get(String key, byte[] defValue) {",
      "      return parseObject.getBytes(key);",
      "  }\n",

      "  private Date _get(String key, Date defValue) {",
      "      return parseObject.getDate(key);",
      "  }\n",

      "  private double _get(String key, Double defValue) {",
      "      return parseObject.getDouble(key);",
      "  }\n",

      "  private int _get(String key, Integer defValue) {",
      "      return parseObject.getInt(key);",
      "  }\n",

      "  private JSONArray _get(String key, JSONArray defValue) {",
      "      return parseObject.getJSONArray(key);",
      "  }\n",

      "  private JSONObject _get(String key, JSONObject defValue) {",
      "      return parseObject.getJSONObject(key);",
      "  }\n",

      "  @SuppressWarnings(\"unchecked\")",
      "  private  List _get(String key, List defValue) {",
      "      return (List) parseObject.getList(key);",
      "  }\n",

      "  @SuppressWarnings(\"unchecked\")",
      "  private  Map _get(String key, Map defValue) {",
      "      return (Map) parseObject.getMap(key);",
      "  }\n",

      "  @SuppressWarnings(\"unchecked\")",
      "  private  ParseRelation _get(String key, ParseRelation defValue) {",
      "      return (ParseRelation) parseObject.getRelation(key);",
      "  }\n",

      "  private long _get(String key, Long defValue) {",
      "      return parseObject.getLong(key);",
      "  }\n",

      "  private Number _get(String key, Number defValue) {",
      "      return parseObject.getNumber(key);",
      "  }\n",

      "  private ParseFile _get(String key, ParseFile defValue) {",
      "      return parseObject.getParseFile(key);",
      "  }\n",

      "  private ParseGeoPoint _get(String key, ParseGeoPoint defValue) {",
      "      return parseObject.getParseGeoPoint(key);",
      "  }\n",

      "  private ParseObject _get(String key, ParseObject defValue) {",
      "      return parseObject.getParseObject(key);",
      "  }\n",

      "  private ParseUser _get(String key, ParseUser defValue) {",
      "      return parseObject.getParseUser(key);",
      "  }\n",

      "  private String _get(String key, String defValue) {",
      "      return parseObject.getString(key);",
      "  }",

      // Property getters
      "$[props:p|\n|\n  @Override",
      "  $[p.access]$[p.type] $[p]() {",
      "    return $[p.array?[$[p.nullable?$[p] == null ? null : ]$[p].clone()][_get(\"$[p]\", ($[p.type]) null)]];",
      "  }]",

      // toString()
      "$[toString?\n  @Override",
      "  public String toString() {",
      "    return \"$[simpleclassname]{\"$[props?\n        + \"]" +
      "$[props:p|\n        + \", |" +
                "$[p]=\" + $[p.array?[$[Arrays].toString($[p])][$[p]]]]",
      "        + \"}\";",
      "  }]",

      // equals(Object)
      "$[equals?\n  @Override",
      "  public boolean equals(Object o) {",
      "    if (o == this) {",
      "      return true;",
      "    }",
      "    if (o instanceof $[origclass]) {",
      "      $[origclass]$[wildcardtypes] that = ($[origclass]$[wildcardtypes]) o;",
      "      return $[props!true]" +
                   "$[props:p|\n          && |($[p.equalsThatExpression])];",
      "    }",
      "    return false;",
      "  }]",

      // hashCode()
      "$[hashCode?",
      "$[cacheHashCode?  private transient int hashCode;\n\n]" +

      "  @Override",
      "  public int hashCode() {",
      "$[cacheHashCode?    if (hashCode != 0) {",
      "      return hashCode;",
      "    }\n]" +
      "    int h = 1;",
      "$[props:p||" +
      "    h *= 1000003;",
      "    h ^= $[p.hashCodeExpression];",
      "]" +
      "$[cacheHashCode?    hashCode = h;\n]" +
      "    return h;",
      "  }]" +

      // serialVersionUID
      "$[serialVersionUID?\n\n  private static final long serialVersionUID = $[serialVersionUID];]",

      // parcelable
      "$[parcelable?\n\n",
      "  public static final android.os.Parcelable.Creator<$[origclass]> CREATOR = new android.os.Parcelable.Creator<$[origclass]>() {",
      "    @Override public $[origclass] createFromParcel(android.os.Parcel in) {",
      "      return new $[subclass](in);",
      "    }",
      "    @Override public $[origclass][] newArray(int size) {",
      "      return new $[origclass][size];",
      "    }",
      "  };",
      "",
      "  private final static java.lang.ClassLoader CL = $[subclass].class.getClassLoader();",
      "",
      "  private $[subclass](android.os.Parcel in) {",
      "    this(\n      $[props:p|,\n      |($[p.castType]) in.readValue(CL)]);",
      "  }",
      "",
      "  @Override public void writeToParcel(android.os.Parcel dest, int flags) {",
      "$[props:p||    dest.writeValue($[p]);\n]",
      "  }",
      "",
      "  @Override public int describeContents() {",
      "    return 0;",
      "  }",
      "]",

      "}"
      // CHECKSTYLE:ON
  );
  private static final Template template = Template.compile(TEMPLATE_STRING);

  static class Property {
    private final ExecutableElement method;
    private final String type;
    private final Map vars;

    Property(ExecutableElement method, String type, Map vars) {
      this.method = method;
      this.type = type;
      this.vars = vars;
    }

    @Override
    public String toString() {
      return method.getSimpleName().toString();
    }

    TypeElement owner() {
      return (TypeElement) method.getEnclosingElement();
    }

    public String type() {
      return type;
    }

    // That wouldn't be necessary if we supported Java 7+. Oh well.
    public String castType() {
      return primitive() ? box(method.getReturnType().getKind()) : type();
    }

    private String box(TypeKind kind) {
      switch(kind) {
        case BOOLEAN: return "Boolean";
        case BYTE: return "Byte";
        case SHORT: return "Short";
        case INT: return "Integer";
        case LONG: return "Long";
        case CHAR: return "Character";
        case FLOAT: return "Float";
        case DOUBLE: return "Double";
        default: throw new RuntimeException("Found a new Primitive type on the Java platform. "
            + "Go ahead and claim " + kind + " yours");
      }
    }

    public boolean primitive() {
      return method.getReturnType().getKind().isPrimitive();
    }

    public boolean array() {
      return method.getReturnType().getKind() == TypeKind.ARRAY;
    }

    public boolean nullable() {
      for (AnnotationMirror annotationMirror : method.getAnnotationMirrors()) {
        String name = annotationMirror.getAnnotationType().asElement().getSimpleName().toString();
        if (name.equals("Nullable")) {
          return true;
        }
      }
      return false;
    }

    private static final Template PRIMITIVE_EQUALS_TEMPLATE =
        Template.compile("this.$[p] == that.$[p]()");
    private static final Template ARRAY_EQUALS_TEMPLATE =
        Template.compile("$[Arrays].equals(this.$[p], "
            + "(that instanceof $[subclass]) ? (($[subclass]) that).$[p] : that.$[p]())");
    private static final Template FLOAT_EQUALS_TEMPLATE = Template.compile(
        "Float.floatToIntBits(this.$[p]) == Float.floatToIntBits(that.$[p]())");
    private static final Template DOUBLE_EQUALS_TEMPLATE = Template.compile(
        "Double.doubleToLongBits(this.$[p]) == Double.doubleToLongBits(that.$[p]())");
    // CHECKSTYLE:OFF:OperatorWrap
    private static final Template OBJECT_EQUALS_TEMPLATE = Template.compile(
        "$[p.nullable?" +
          "(this.$[p] == null) ? (that.$[p]() == null) : ]" +
          "this.$[p].equals(that.$[p]())");
    // CHECKSTYLE:ON

    /**
     * A string representing an expression that compares this property with the same property
     * in another variable called "that" whose type is the class marked {@code @AutoParse}.
     */
    public String equalsThatExpression() {
      // If the templating language had a case statement we wouldn't need this function, but the
      // language is unreadable enough as it is.
      Template template;
      switch (method.getReturnType().getKind()) {
        case BYTE:
        case SHORT:
        case CHAR:
        case INT:
        case LONG:
        case BOOLEAN:
          template = PRIMITIVE_EQUALS_TEMPLATE;
          break;
        case FLOAT:
          template = FLOAT_EQUALS_TEMPLATE;
          break;
        case DOUBLE:
          template = DOUBLE_EQUALS_TEMPLATE;
          break;
        case ARRAY:
          template = ARRAY_EQUALS_TEMPLATE;
          break;
        default:
          template = OBJECT_EQUALS_TEMPLATE;
          break;
      }
      Map newVars = new TreeMap(vars);
      newVars.put("p", this);
      return template.rewrite(newVars);
    }

    /**
     * A string representing an expression that is the hashCode of this property.
     */
    public String hashCodeExpression() {
      switch (method.getReturnType().getKind()) {
        case BYTE:
        case SHORT:
        case CHAR:
        case INT:
          return this.toString();
        case LONG:
          return "(" + this + " >>> 32) ^ " + this;
        case FLOAT:
          return "Float.floatToIntBits(" + this + ")";
        case DOUBLE:
          return "(Double.doubleToLongBits(" + this + ") >>> 32) ^ "
              + "Double.doubleToLongBits(" + this + ")";
        case BOOLEAN:
          return this + " ? 1231 : 1237";
        case ARRAY:
          return vars.get("Arrays") + ".hashCode(" + this + ")";
        default:
          if (nullable()) {
            return "(" + this + " == null) ? 0 : " + this + ".hashCode()";
          } else {
            return this + ".hashCode()";
          }
      }
    }

    public String access() {
      Set mods = method.getModifiers();
      if (mods.contains(Modifier.PUBLIC)) {
        return "public ";
      } else if (mods.contains(Modifier.PROTECTED)) {
        return "protected ";
      } else {
        return "";
      }
    }
  }

  private static boolean isJavaLangObject(TypeElement type) {
    return type.getSuperclass().getKind() == TypeKind.NONE && type.getKind() == ElementKind.CLASS;
  }

  private static boolean isToStringOrEqualsOrHashCode(ExecutableElement method) {
    String name = method.getSimpleName().toString();
    return ((name.equals("toString") || name.equals("hashCode"))
              && method.getParameters().isEmpty())
        || (name.equals("equals") && method.getParameters().size() == 1
              && method.getParameters().get(0).asType().toString().equals("java.lang.Object"));
  }

  private void findLocalAndInheritedMethods(TypeElement type, List methods) {
    note("Looking at methods in " + type);
    Types typeUtils = processingEnv.getTypeUtils();
    Elements elementUtils = processingEnv.getElementUtils();
    for (TypeMirror superInterface : type.getInterfaces()) {
      findLocalAndInheritedMethods((TypeElement) typeUtils.asElement(superInterface), methods);
    }
    if (type.getSuperclass().getKind() != TypeKind.NONE) {
      // Visit the superclass after superinterfaces so we will always see the implementation of a
      // method after any interfaces that declared it.
      findLocalAndInheritedMethods(
          (TypeElement) typeUtils.asElement(type.getSuperclass()), methods);
    }
    // Add each method of this class, and in so doing remove any inherited method it overrides.
    // This algorithm is quadratic in the number of methods but it's hard to see how to improve
    // that while still using Elements.overrides.
    List theseMethods = ElementFilter.methodsIn(type.getEnclosedElements());
    eclipseHack().sortMethodsIfSimulatingEclipse(theseMethods);
    for (ExecutableElement method : theseMethods) {
      if (!method.getModifiers().contains(Modifier.PRIVATE)) {
        boolean alreadySeen = false;
        for (Iterator methodIter = methods.iterator(); methodIter.hasNext();) {
          ExecutableElement otherMethod = methodIter.next();
          if (elementUtils.overrides(method, otherMethod, type)) {
            methodIter.remove();
          } else if (method.getSimpleName().equals(otherMethod.getSimpleName())
              && method.getParameters().equals(otherMethod.getParameters())) {
            // If we inherit this method on more than one path, we don't want to add it twice.
            alreadySeen = true;
          }
        }
        if (!alreadySeen) {
          methods.add(method);
        }
      }
    }
  }

  private void processType(TypeElement type) throws CompileException {
    AutoParse autoParse = type.getAnnotation(AutoParse.class);
    if (autoParse == null) {
      // This shouldn't happen unless the compilation environment is buggy,
      // but it has happened in the past and can crash the compiler.
      abortWithError("annotation processor for @AutoParse was invoked with a type that "
          + "does not have that annotation; this is probably a compiler bug", type);
    }
    if (type.getKind() != ElementKind.CLASS) {
      abortWithError("@" + AutoParse.class.getName() + " only applies to classes", type);
    }
    if (ancestorIsAndroidAutoParse(type)) {
      abortWithError("One @AutoParse class may not extend another", type);
    }
    Map vars = new TreeMap();
    vars.put("pkg", TypeSimplifier.packageNameOf(type));
    vars.put("origclass", classNameOf(type));
    vars.put("simpleclassname", simpleNameOf(classNameOf(type)));
    vars.put("formaltypes", formalTypeString(type));
    vars.put("actualtypes", actualTypeString(type));
    vars.put("wildcardtypes", wildcardTypeString(type));
    vars.put("subclass", simpleNameOf(generatedSubclassName(type)));
    vars.put("cacheHashCode", autoParse.cacheHashCode());
    defineVarsForType(type, vars);
    String text = template.rewrite(vars);
    writeSourceFile(generatedSubclassName(type), text, type);
  }

  private void defineVarsForType(TypeElement type, Map vars)
      throws CompileException {
    List methods = new ArrayList();
    findLocalAndInheritedMethods(type, methods);
    vars.putAll(objectMethodsToGenerate(methods));
    dontImplementAnnotationEqualsOrHashCode(type, vars);
    List toImplement = methodsToImplement(methods);
    Set types = new HashSet();
    types.addAll(returnTypesOf(toImplement));
    TypeMirror javaUtilArrays = getTypeMirror(Arrays.class);
    if (containsArrayType(types)) {
      // If there are array properties then we will be referencing java.util.Arrays.
      // Arrange to import it unless that would introduce ambiguity.
      types.add(javaUtilArrays);
    }
    String pkg = TypeSimplifier.packageNameOf(type);
    TypeSimplifier typeSimplifier = new TypeSimplifier(processingEnv.getTypeUtils(), pkg, types);
    vars.put("imports", typeSimplifier.typesToImport());
    vars.put("Arrays", typeSimplifier.simplify(javaUtilArrays));
    List props = new ArrayList();
    for (ExecutableElement method : toImplement) {
      String propType = typeSimplifier.simplify(method.getReturnType());
      Property prop = new Property(method, propType, vars);
      props.add(prop);
    }
    // If we are running from Eclipse, undo the work of its compiler which sorts methods.
    eclipseHack().reorderProperties(props);
    vars.put("props", props);
    vars.put("serialVersionUID", getSerialVersionUID(type));

    TypeMirror parcelable = getTypeMirror("android.os.Parcelable");
    vars.put("parcelable", processingEnv.getTypeUtils().isAssignable(type.asType(), parcelable));
  }

  private Set returnTypesOf(List methods) {
    HashSet returnTypes = new HashSet();
    for (ExecutableElement method : methods) {
      returnTypes.add(method.getReturnType());
    }
    return returnTypes;
  }

  private static boolean containsArrayType(Set types) {
    for (TypeMirror type : types) {
      if (type.getKind() == TypeKind.ARRAY) {
        return true;
      }
    }
    return false;
  }

  private void dontImplementAnnotationEqualsOrHashCode(TypeElement type, Map vars) {
    TypeMirror javaLangAnnotationAnnotation = getTypeMirror(Annotation.class);
    Types typeUtils = processingEnv.getTypeUtils();
    if (typeUtils.isAssignable(type.asType(), javaLangAnnotationAnnotation)) {
      boolean equals = (Boolean) vars.get("equals");
      boolean hashCode = (Boolean) vars.get("hashCode");
      if (equals || hashCode) {
        String bad = equals
            ? (hashCode ? "equals(Object) and hashCode()" : "equals(Object)")
            : "hashCode()";
        reportError("The implementation of " + bad + " that would be generated for this @AutoParse "
            + "class would not obey the contract of " + bad + " in " + Annotation.class.getName(),
            type);
      }
    }
  }

  /**
   * Given a list of all methods defined in or inherited by a class, returns a map with keys
   * "toString", "equals", "hashCode" and corresponding value true if that method should be
   * generated.
   */
  private static Map objectMethodsToGenerate(List methods) {
    Map vars = new TreeMap();
    // The defaults here only come into play when an ancestor class doesn't exist.
    // Compilation will fail in that case, but we don't want it to crash the compiler with
    // an exception before it does. If all ancestors do exist then we will definitely find
    // definitions of these three methods (perhaps the ones in Object) so we will overwrite these:
    vars.put("equals", false);
    vars.put("hashCode", false);
    vars.put("toString", false);
    for (ExecutableElement method : methods) {
      if (isToStringOrEqualsOrHashCode(method)) {
        boolean canGenerate = method.getModifiers().contains(Modifier.ABSTRACT)
            || isJavaLangObject((TypeElement) method.getEnclosingElement());
        vars.put(method.getSimpleName().toString(), canGenerate);
      }
    }
    assert vars.size() == 3;
    return vars;
  }

  private List methodsToImplement(List methods)
      throws CompileException {
    List toImplement = new ArrayList();
    boolean errors = false;
    for (ExecutableElement method : methods) {
      if (method.getModifiers().contains(Modifier.ABSTRACT)
          && !isToStringOrEqualsOrHashCode(method) && !isFromParcelable(method)) {
        if (method.getParameters().isEmpty() && method.getReturnType().getKind() != TypeKind.VOID) {
          if (isReferenceArrayType(method.getReturnType())) {
            reportError("An @AutoParse class cannot define an array-valued property unless it is "
                + "a primitive array", method);
            errors = true;
          }
          toImplement.add(method);
        } else {
          reportError("@AutoParse classes cannot have abstract methods other than property getters",
              method);
          errors = true;
        }
      }
    }
    if (errors) {
      throw new CompileException();
    }
    return toImplement;
  }

  private boolean isFromParcelable(ExecutableElement method) {
    String name = method.getSimpleName().toString();
    boolean isDescribeContents = name.equals("describeContents") && method.getParameters().isEmpty()
        && method.getReturnType().toString().equals("int");
    boolean isWriteToParcel = name.equals("writeToParcel") && method.getParameters().size() == 2
        && method.getReturnType().toString().equals("void")
        && method.getParameters().get(0).asType().toString().equals("android.os.Parcel")
        && method.getParameters().get(1).asType().toString().equals("int");
    return isDescribeContents || isWriteToParcel;
  }

  private static boolean isReferenceArrayType(TypeMirror type) {
    return type.getKind() == TypeKind.ARRAY
        && !((ArrayType) type).getComponentType().getKind().isPrimitive();
  }

  private void writeSourceFile(String className, String text, TypeElement originatingType) {
    try {
      note(text);
      JavaFileObject sourceFile =
          processingEnv.getFiler().createSourceFile(className, originatingType);
      Writer writer = sourceFile.openWriter();
      try {
        writer.write(text);
      } finally {
        writer.close();
      }
    } catch (IOException e) {
      processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
          "Could not write generated class " + className + ": " + e);
    }
  }

  private boolean ancestorIsAndroidAutoParse(TypeElement type) {
    while (true) {
      TypeMirror parentMirror = type.getSuperclass();
      if (parentMirror.getKind() == TypeKind.NONE) {
        return false;
      }
      Types typeUtils = processingEnv.getTypeUtils();
      TypeElement parentElement = (TypeElement) typeUtils.asElement(parentMirror);
      if (parentElement.getAnnotation(AutoParse.class) != null) {
        return true;
      }
      type = parentElement;
    }
  }

  // Return a string like "1234L" if type instanceof Serializable and defines
  // serialVersionUID = 1234L, otherwise "".
  private String getSerialVersionUID(TypeElement type) {
    Types typeUtils = processingEnv.getTypeUtils();
    TypeMirror serializable = getTypeMirror(Serializable.class);
    if (typeUtils.isAssignable(type.asType(), serializable)) {
      List fields = ElementFilter.fieldsIn(type.getEnclosedElements());
      for (VariableElement field : fields) {
        if (field.getSimpleName().toString().equals("serialVersionUID")) {
          Object value = field.getConstantValue();
          if (field.getModifiers().containsAll(Arrays.asList(Modifier.STATIC, Modifier.FINAL))
              && field.asType().getKind() == TypeKind.LONG
              && value != null) {
            return value + "L";
          } else {
            reportError(
                "serialVersionUID must be a static final long compile-time constant", field);
            break;
          }
        }
      }
    }
    return "";
  }

  private TypeMirror getTypeMirror(Class c) {
    return getTypeMirror(c.getName());
  }

  private TypeMirror getTypeMirror(String className) {
    return processingEnv.getElementUtils().getTypeElement(className).asType();
  }

  // Why does TypeParameterElement.toString() not return this? Grrr.
  private static String typeParameterString(TypeParameterElement type) {
    String s = type.getSimpleName().toString();
    List bounds = type.getBounds();
    if (bounds.isEmpty()) {
      return s;
    } else {
      s += " extends ";
      String sep = "";
      for (TypeMirror bound : bounds) {
        s += sep + bound;
        sep = " & ";
      }
      return s;
    }
  }

  private static String formalTypeString(TypeElement type) {
    List typeParameters = type.getTypeParameters();
    if (typeParameters.isEmpty()) {
      return "";
    } else {
      String s = "<";
      String sep = "";
      for (TypeParameterElement typeParameter : typeParameters) {
        s += sep + typeParameterString(typeParameter);
        sep = ", ";
      }
      return s + ">";
    }
  }

  private static String actualTypeString(TypeElement type) {
    List typeParameters = type.getTypeParameters();
    if (typeParameters.isEmpty()) {
      return "";
    } else {
      String s = "<";
      String sep = "";
      for (TypeParameterElement typeParameter : typeParameters) {
        s += sep + typeParameter.getSimpleName();
        sep = ", ";
      }
      return s + ">";
    }
  }

  // The @AutoParse type, with a ? for every type.
  private static String wildcardTypeString(TypeElement type) {
    List typeParameters = type.getTypeParameters();
    if (typeParameters.isEmpty()) {
      return "";
    } else {
      String s = "<";
      String sep = "";
      for (int i = 0; i < typeParameters.size(); i++) {
        s += sep + "?";
        sep = ", ";
      }
      return s + ">";
    }
  }

  private EclipseHack eclipseHack() {
    return new EclipseHack(processingEnv);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy