Add proper keep list generation.

Change-Id: I904336e64316318a1a7808c593fc9dfd10019820
diff --git a/src/com/google/doclava/Doclava.java b/src/com/google/doclava/Doclava.java
index 56acf26..9e138ca 100644
--- a/src/com/google/doclava/Doclava.java
+++ b/src/com/google/doclava/Doclava.java
@@ -119,6 +119,7 @@
   public static boolean start(RootDoc r) {
     long startTime = System.nanoTime();
     String keepListFile = null;
+    String proguardFile = null;
     String proofreadFile = null;
     String todoFile = null;
     String sdkValuePath = null;
@@ -170,6 +171,8 @@
         }
       } else if (a[0].equals("-keeplist")) {
         keepListFile = a[1];
+      } else if (a[0].equals("-proguard")) {
+          proguardFile = a[1];
       } else if (a[0].equals("-proofread")) {
         proofreadFile = a[1];
       } else if (a[0].equals("-todo")) {
@@ -303,8 +306,8 @@
     }
 
     // Stubs
-    if (stubsDir != null || apiFile != null) {
-      Stubs.writeStubsAndApi(stubsDir, apiFile, stubPackages);
+    if (stubsDir != null || apiFile != null || proguardFile != null) {
+      Stubs.writeStubsAndApi(stubsDir, apiFile, proguardFile, stubPackages);
     }
 
     Errors.printErrors();
@@ -478,6 +481,9 @@
     if (option.equals("-keeplist")) {
       return 2;
     }
+    if (option.equals("-proguard")) {
+      return 2;
+    }
     if (option.equals("-proofread")) {
       return 2;
     }
diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java
index b493aa0..3c63acd 100644
--- a/src/com/google/doclava/Stubs.java
+++ b/src/com/google/doclava/Stubs.java
@@ -31,12 +31,13 @@
 import java.util.Set;
 
 public class Stubs {
-  public static void writeStubsAndApi(String stubsDir, String apiFile,
+  public static void writeStubsAndApi(String stubsDir, String apiFile, String keepListFile,
       HashSet<String> stubPackages) {
     // figure out which classes we need
     final HashSet<ClassInfo> notStrippable = new HashSet<ClassInfo>();
     ClassInfo[] all = Converter.allClasses();
     PrintStream apiWriter = null;
+    PrintStream keepListWriter = null;
     if (apiFile != null) {
       try {
         File xml = new File(apiFile);
@@ -47,6 +48,16 @@
             "Cannot open file for write.");
       }
     }
+    if (keepListFile != null) {
+      try {
+        File keepList = new File(keepListFile);
+        keepList.getParentFile().mkdirs();
+        keepListWriter = new PrintStream(new BufferedOutputStream(new FileOutputStream(keepList)));
+      } catch (FileNotFoundException e) {
+        Errors.error(Errors.IO_ERROR, new SourcePositionInfo(keepListFile, 0, 0),
+            "Cannot open file for write.");
+      }
+    }
     // If a class is public or protected, not hidden, and marked as included,
     // then we can't strip it
     for (ClassInfo cl : all) {
@@ -125,8 +136,8 @@
           if (stubsDir != null) {
             writeClassFile(stubsDir, notStrippable, cl);
           }
-          // build class list for xml file
-          if (apiWriter != null) {
+          // build class list for api file or keep list file
+          if (apiWriter != null || keepListWriter != null) {
             if (packages.containsKey(cl.containingPackage())) {
               packages.get(cl.containingPackage()).add(cl);
             } else {
@@ -144,6 +155,12 @@
       writeApi(apiWriter, packages, notStrippable);
       apiWriter.close();
     }
+
+    // write out the keep list
+    if (keepListWriter != null) {
+      writeKeepList(keepListWriter, packages, notStrippable);
+      keepListWriter.close();
+    }
   }
 
   public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable, String why) {
@@ -1235,6 +1252,148 @@
     apiWriter.print("\n");
   }
 
+  static void writeKeepList(PrintStream keepListWriter,
+      HashMap<PackageInfo, List<ClassInfo>> allClasses, HashSet<ClassInfo> notStrippable) {
+    // extract the set of packages, sort them by name, and write them out in that order
+    Set<PackageInfo> allClassKeys = allClasses.keySet();
+    PackageInfo[] allPackages = allClassKeys.toArray(new PackageInfo[allClassKeys.size()]);
+    Arrays.sort(allPackages, PackageInfo.comparator);
+
+    for (PackageInfo pack : allPackages) {
+      writePackageKeepList(keepListWriter, pack, allClasses.get(pack), notStrippable);
+    }
+  }
+
+  static void writePackageKeepList(PrintStream keepListWriter, PackageInfo pack,
+      Collection<ClassInfo> classList, HashSet<ClassInfo> notStrippable) {
+    // Work around the bogus "Array" class we invent for
+    // Arrays.copyOf's Class<? extends T[]> newType parameter. (http://b/2715505)
+    if (pack.name().equals(PackageInfo.DEFAULT_PACKAGE)) {
+      return;
+    }
+
+    ClassInfo[] classes = classList.toArray(new ClassInfo[classList.size()]);
+    Arrays.sort(classes, ClassInfo.comparator);
+    for (ClassInfo cl : classes) {
+      writeClassKeepList(keepListWriter, cl, notStrippable);
+    }
+  }
+
+  static void writeClassKeepList(PrintStream keepListWriter, ClassInfo cl,
+      HashSet<ClassInfo> notStrippable) {
+    keepListWriter.print("-keep class ");
+    keepListWriter.print(to$Class(cl.qualifiedName()));
+
+    keepListWriter.print(" {\n");
+
+    ArrayList<MethodInfo> constructors = cl.constructors();
+    Collections.sort(constructors, MethodInfo.comparator);
+    for (MethodInfo mi : constructors) {
+      writeConstructorKeepList(keepListWriter, mi);
+    }
+
+    keepListWriter.print("\n");
+
+    ArrayList<MethodInfo> methods = cl.allSelfMethods();
+    Collections.sort(methods, MethodInfo.comparator);
+    for (MethodInfo mi : methods) {
+      if (!methodIsOverride(notStrippable, mi)) {
+        writeMethodKeepList(keepListWriter, mi);
+      }
+    }
+
+    keepListWriter.print("\n");
+
+    ArrayList<FieldInfo> enums = cl.enumConstants();
+    Collections.sort(enums, FieldInfo.comparator);
+    for (FieldInfo fi : enums) {
+      writeFieldKeepList(keepListWriter, fi);
+    }
+
+    keepListWriter.print("\n");
+
+    ArrayList<FieldInfo> fields = cl.allSelfFields();
+    Collections.sort(fields, FieldInfo.comparator);
+    for (FieldInfo fi : fields) {
+      writeFieldKeepList(keepListWriter, fi);
+    }
+
+    keepListWriter.print("}\n\n");
+  }
+
+  static void writeConstructorKeepList(PrintStream keepListWriter, MethodInfo mi) {
+    keepListWriter.print("    ");
+    String name = mi.name();
+    name = name.replace(".", "$");
+    keepListWriter.print(name);
+
+    writeParametersKeepList(keepListWriter, mi, mi.parameters());
+    keepListWriter.print(";\n");
+  }
+
+  static void writeMethodKeepList(PrintStream keepListWriter, MethodInfo mi) {
+    keepListWriter.print("    ");
+    keepListWriter.print(mi.scope());
+    if (mi.isStatic()) {
+      keepListWriter.print(" static");
+    }
+    if (mi.isAbstract()) {
+      keepListWriter.print(" abstract");
+    }
+    if (mi.isSynchronized()) {
+      keepListWriter.print(" synchronized");
+    }
+    keepListWriter.print(" ");
+    if (mi.returnType() == null) {
+      keepListWriter.print("void");
+    } else {
+      keepListWriter.print(getCleanTypeName(mi.returnType()));
+    }
+    keepListWriter.print(" ");
+    keepListWriter.print(mi.name());
+
+    writeParametersKeepList(keepListWriter, mi, mi.parameters());
+
+    keepListWriter.print(";\n");
+  }
+
+  static void writeParametersKeepList(PrintStream keepListWriter, MethodInfo method,
+      ArrayList<ParameterInfo> params) {
+    keepListWriter.print("(");
+
+    for (ParameterInfo pi : params) {
+      if (pi != params.get(0)) {
+        keepListWriter.print(", ");
+      }
+      keepListWriter.print(fullParameterTypeNameNoGenerics(method, pi.type(),
+          pi == params.get(params.size() - 1)));
+    }
+
+    keepListWriter.print(")");
+  }
+
+  static void writeFieldKeepList(PrintStream keepListWriter, FieldInfo fi) {
+    keepListWriter.print("    ");
+    keepListWriter.print(fi.scope());
+    if (fi.isStatic()) {
+      keepListWriter.print(" static");
+    }
+    if (fi.isTransient()) {
+      keepListWriter.print(" transient");
+    }
+    if (fi.isVolatile()) {
+      keepListWriter.print(" volatile");
+    }
+
+    keepListWriter.print(" ");
+    keepListWriter.print(getCleanTypeName(fi.type()) + fi.type().dimension());
+
+    keepListWriter.print(" ");
+    keepListWriter.print(fi.name());
+
+    keepListWriter.print(";\n");
+  }
+
   static String fullParameterTypeName(MethodInfo method, TypeInfo type, boolean isLast) {
     String fullTypeName = type.fullName(method.typeVariables());
     if (isLast && method.isVarArgs()) {
@@ -1245,4 +1404,31 @@
     }
     return fullTypeName;
   }
+
+  static String fullParameterTypeNameNoGenerics(MethodInfo method, TypeInfo type, boolean isLast) {
+    String fullTypeName = getCleanTypeName(type);
+    if (isLast && method.isVarArgs()) {
+      // TODO: note that this does not attempt to handle hypothetical
+      // vararg methods whose last parameter is a list of arrays, e.g.
+      // "Object[]...".
+      fullTypeName = type.fullNameNoDimension(method.typeVariables()) + "...";
+    }
+    return fullTypeName;
+  }
+
+  static String to$Class(String name) {
+    int pos = 0;
+    while ((pos = name.indexOf('.', pos)) > 0) {
+      String n = name.substring(0, pos);
+      if (Converter.obtainClass(n) != null) {
+        return n + (name.substring(pos).replace('.', '$'));
+      }
+      pos = pos + 1;
+    }
+    return name;
+  }
+
+  static String getCleanTypeName(TypeInfo t) {
+      return t.isPrimitive() ? t.simpleTypeName() : to$Class(t.asClassInfo().qualifiedName());
+  }
 }