add new option to ApiCheck to look for new API methods

When 'newapi' option is needed, ApiCheck will be in "diff" mode:
* when classes are checked for consistency, newly added methods
  and constructors are kept in lists
* when packages are checked for consistency, newly added classes
  and classes with new API methods are kept in list
* when APIs are checked for consistency, newly added packages
  and pakages with new/modified classes are added
* the accumulated deltas are then exported in XML format

Change-Id: I3fed989e2836109e334c0e665639190196f14f4c
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index 8dde5c6..374b7b6 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -2045,7 +2045,12 @@
   }
 
   public boolean isConsistent(ClassInfo cl) {
+    return isConsistent(cl, null, null);
+  }
+
+  public boolean isConsistent(ClassInfo cl, List<MethodInfo> newCtors, List<MethodInfo> newMethods) {
     boolean consistent = true;
+    boolean diffMode = (newCtors != null) && (newMethods != null);
 
     if (isInterface() != cl.isInterface()) {
       Errors.error(Errors.CHANGED_CLASS, cl.position(), "Class " + cl.qualifiedName()
@@ -2100,10 +2105,16 @@
             mi.isAbstract() != mInfo.isAbstract()) {
           Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public method "
               + mInfo.qualifiedName());
+          if (diffMode) {
+            newMethods.add(mInfo);
+          }
           consistent = false;
         }
       }
     }
+    if (diffMode) {
+      Collections.sort(newMethods, MethodInfo.comparator);
+    }
 
     for (MethodInfo mInfo : mApiCheckConstructors.values()) {
       if (cl.mApiCheckConstructors.containsKey(mInfo.getHashableName())) {
@@ -2120,9 +2131,15 @@
       if (!mApiCheckConstructors.containsKey(mInfo.getHashableName())) {
         Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public constructor "
             + mInfo.prettySignature());
+        if (diffMode) {
+          newCtors.add(mInfo);
+        }
         consistent = false;
       }
     }
+    if (diffMode) {
+      Collections.sort(newCtors, MethodInfo.comparator);
+    }
 
     for (FieldInfo mInfo : mApiCheckFields.values()) {
       if (cl.mApiCheckFields.containsKey(mInfo.name())) {
diff --git a/src/com/google/doclava/PackageInfo.java b/src/com/google/doclava/PackageInfo.java
index 02beaf7..12f18b3 100644
--- a/src/com/google/doclava/PackageInfo.java
+++ b/src/com/google/doclava/PackageInfo.java
@@ -18,8 +18,8 @@
 
 import com.google.doclava.apicheck.ApiInfo;
 import com.google.clearsilver.jsilver.data.Data;
-
 import com.sun.javadoc.*;
+
 import java.util.*;
 
 public class PackageInfo extends DocInfo implements ContainerInfo {
@@ -395,12 +395,85 @@
   }
 
   public boolean isConsistent(PackageInfo pInfo) {
+    return isConsistent(pInfo, null);
+  }
+
+  /**
+   * Creates the delta class by copying class signatures from original, but use provided list of
+   * constructors and methods.
+   */
+  private ClassInfo createDeltaClass(ClassInfo original,
+      ArrayList<MethodInfo> constructors, ArrayList<MethodInfo> methods) {
+    ArrayList<FieldInfo> emptyFields = new ArrayList<>();
+    ArrayList<ClassInfo> emptyClasses = new ArrayList<>();
+    ArrayList<TypeInfo> emptyTypes = new ArrayList<>();
+    ArrayList<MethodInfo> emptyMethods = new ArrayList<>();
+    ClassInfo ret = new ClassInfo(null, original.getRawCommentText(), original.position(),
+        original.isPublic(), original.isProtected(), original.isPackagePrivate(),
+        original.isPrivate(), original.isStatic(), original.isInterface(),
+        original.isAbstract(), original.isOrdinaryClass(),
+        original.isException(), original.isError(), original.isEnum(), original.isAnnotation(),
+        original.isFinal(), original.isIncluded(), original.name(), original.qualifiedName(),
+        original.qualifiedTypeName(), original.isPrimitive());
+    ArrayList<ClassInfo> interfaces = original.interfaces();
+    // avoid providing null to init method, replace with empty array list when needed
+    if (interfaces == null) {
+      interfaces = emptyClasses;
+    }
+    ArrayList<TypeInfo> interfaceTypes = original.interfaceTypes();
+    if (interfaceTypes == null) {
+      interfaceTypes = emptyTypes;
+    }
+    ArrayList<ClassInfo> innerClasses = original.innerClasses();
+    if (innerClasses == null) {
+      innerClasses = emptyClasses;
+    }
+    ArrayList<MethodInfo> annotationElements = original.annotationElements();
+    if (annotationElements == null) {
+      annotationElements = emptyMethods;
+    }
+    ArrayList<AnnotationInstanceInfo> annotations = original.annotations();
+    if (annotations == null) {
+      annotations = new ArrayList<>();
+    }
+    ret.init(original.type(), interfaces, interfaceTypes, innerClasses,
+        constructors, methods, annotationElements,
+        emptyFields /* fields */, emptyFields /* enum */,
+        original.containingPackage(), original.containingClass(), original.superclass(),
+        original.superclassType(), annotations);
+    return ret;
+  }
+
+  /**
+   * Check if packages are consistent, also record class deltas.
+   * <p>
+   * <ul>class deltas are:
+   * <li>brand new classes that are not present in current package
+   * <li>stripped existing classes stripped where only newly added methods are kept
+   * @param pInfo
+   * @param clsInfoDiff
+   * @return
+   */
+  public boolean isConsistent(PackageInfo pInfo, List<ClassInfo> clsInfoDiff) {
     boolean consistent = true;
+    boolean diffMode = clsInfoDiff != null;
     for (ClassInfo cInfo : mClasses.values()) {
+      ArrayList<MethodInfo> newClsApis = null;
+      ArrayList<MethodInfo> newClsCtors = null;
       if (pInfo.mClasses.containsKey(cInfo.name())) {
-        if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()))) {
+        if (diffMode) {
+          newClsApis = new ArrayList<>();
+          newClsCtors = new ArrayList<>();
+        }
+        if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()), newClsCtors, newClsApis)) {
           consistent = false;
         }
+        // if we are in diff mode, add class to list if there's new ctor or new apis
+        if (diffMode && !(newClsCtors.isEmpty() && newClsApis.isEmpty())) {
+          // generate a "delta" class with only added methods and constructors, but no fields etc
+          ClassInfo deltaClsInfo = createDeltaClass(cInfo, newClsCtors, newClsApis);
+          clsInfoDiff.add(deltaClsInfo);
+        }
       } else {
         Errors.error(Errors.REMOVED_CLASS, cInfo.position(), "Removed public class "
             + cInfo.qualifiedName());
@@ -412,8 +485,15 @@
         Errors.error(Errors.ADDED_CLASS, cInfo.position(), "Added class " + cInfo.name()
             + " to package " + pInfo.name());
         consistent = false;
+        // brand new class, add everything as is
+        if (diffMode) {
+            clsInfoDiff.add(cInfo);
+        }
       }
     }
+    if (diffMode) {
+      Collections.sort(clsInfoDiff, ClassInfo.comparator);
+    }
     return consistent;
   }
 }
diff --git a/src/com/google/doclava/apicheck/ApiCheck.java b/src/com/google/doclava/apicheck/ApiCheck.java
index 28d7ce0..521ac52 100644
--- a/src/com/google/doclava/apicheck/ApiCheck.java
+++ b/src/com/google/doclava/apicheck/ApiCheck.java
@@ -23,10 +23,12 @@
 import java.io.PrintStream;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 import java.util.Stack;
 
 import com.google.doclava.Errors;
+import com.google.doclava.PackageInfo;
 import com.google.doclava.Errors.ErrorMessage;
 import com.google.doclava.Stubs;
 
@@ -62,6 +64,9 @@
       System.exit(convertToApi(originalArgs[1], originalArgs[2]));
     } else if (originalArgs.length == 3 && "-convert2xml".equals(originalArgs[0])) {
       System.exit(convertToXml(originalArgs[1], originalArgs[2]));
+    } else if (originalArgs.length == 4 && "-new_api".equals(originalArgs[0])) {
+      // command syntax: -new_api oldapi.txt newapi.txt diff.xml
+      System.exit(newApi(originalArgs[1], originalArgs[2], originalArgs[3]));
     } else {
       ApiCheck acheck = new ApiCheck();
       Report report = acheck.checkApi(originalArgs);
@@ -270,4 +275,41 @@
     return 0;
   }
 
+  /**
+   * Generates a "diff": where new API is trimmed down by removing existing methods found in old API
+   * @param origApiPath path to old API text file
+   * @param newApiPath path to new API text file
+   * @param outputPath output XML path for the generated diff
+   * @return
+   */
+  static int newApi(String origApiPath, String newApiPath, String outputPath) {
+    ApiInfo origApi, newApi;
+    try {
+      origApi = parseApi(origApiPath);
+    } catch (ApiParseException e) {
+      e.printStackTrace();
+      System.err.println("Error parsing API: " + origApiPath);
+      return 1;
+    }
+    try {
+      newApi = parseApi(newApiPath);
+    } catch (ApiParseException e) {
+      e.printStackTrace();
+      System.err.println("Error parsing API: " + newApiPath);
+      return 1;
+    }
+    List<PackageInfo> pkgInfoDiff = new ArrayList<>();
+    if (!origApi.isConsistent(newApi, pkgInfoDiff)) {
+      PrintStream apiWriter = null;
+      try {
+        apiWriter = new PrintStream(outputPath);
+      } catch (FileNotFoundException ex) {
+        System.err.println("can't open file: " + outputPath);
+      }
+      Stubs.writeXml(apiWriter, pkgInfoDiff);
+    } else {
+      System.err.println("No API change detected, not generating diff.");
+    }
+    return 0;
+  }
 }
diff --git a/src/com/google/doclava/apicheck/ApiInfo.java b/src/com/google/doclava/apicheck/ApiInfo.java
index 711a9f4..148da35 100644
--- a/src/com/google/doclava/apicheck/ApiInfo.java
+++ b/src/com/google/doclava/apicheck/ApiInfo.java
@@ -19,8 +19,11 @@
 import com.google.doclava.ClassInfo;
 import com.google.doclava.Errors;
 import com.google.doclava.PackageInfo;
+
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 public class ApiInfo {
@@ -60,12 +63,28 @@
    * Checks to see if this api is consistent with a newer version.
    */
   public boolean isConsistent(ApiInfo otherApi) {
+    return isConsistent(otherApi, null);
+  }
+
+  public boolean isConsistent(ApiInfo otherApi, List<PackageInfo> pkgInfoDiff) {
     boolean consistent = true;
+    boolean diffMode = pkgInfoDiff != null;
     for (PackageInfo pInfo : mPackages.values()) {
+      List<ClassInfo> newClsApis = null;
       if (otherApi.getPackages().containsKey(pInfo.name())) {
-        if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()))) {
+        if (diffMode) {
+          newClsApis = new ArrayList<>();
+        }
+        if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()), newClsApis)) {
           consistent = false;
         }
+        if (diffMode && !newClsApis.isEmpty()) {
+          PackageInfo info = new PackageInfo(pInfo.name(), pInfo.position());
+          for (ClassInfo cInfo : newClsApis) {
+            info.addClass(cInfo);
+          }
+          pkgInfoDiff.add(info);
+        }
       } else {
         Errors.error(Errors.REMOVED_PACKAGE, pInfo.position(), "Removed package " + pInfo.name());
         consistent = false;
@@ -75,8 +94,14 @@
       if (!mPackages.containsKey(pInfo.name())) {
         Errors.error(Errors.ADDED_PACKAGE, pInfo.position(), "Added package " + pInfo.name());
         consistent = false;
+        if (diffMode) {
+          pkgInfoDiff.add(pInfo);
+        }
       }
     }
+    if (diffMode) {
+      Collections.sort(pkgInfoDiff, PackageInfo.comparator);
+    }
     return consistent;
   }