am 54004373: Fix handling of double-byte chars for tags, keywords.

* commit '5400437364666d0a7d3bfd6ea721fe15e813d657':
  Fix handling of double-byte chars for tags, keywords.
diff --git a/build.gradle b/build.gradle
index 79d856e..50c36d4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,7 @@
 apply plugin: 'java'
 
+import com.android.internal.BuildUtils
+
 sourceSets {
     main {
         java {
@@ -10,20 +12,9 @@
         }
     }
 }
-// TODO put this function in a plugin
-String findToolsJar() {
-    new ByteArrayOutputStream().withStream { os ->
-        project.exec {
-            executable "$rootDir/build/core/find-jdk-tools-jar.sh"
-
-            standardOutput = os
-        }
-        return os.toString().trim()
-    }
-}
 
 dependencies {
-    compile files(findToolsJar())
+    compile files(BuildUtils.findToolsJar(project))
     compile project(path: ':antlr', configuration: 'antlrRuntime')
     compile project(':jsilver')
     compile project(':tagsoup')
diff --git a/src/com/google/doclava/AttrTagInfo.java b/src/com/google/doclava/AttrTagInfo.java
index 909cacf..04e5626 100644
--- a/src/com/google/doclava/AttrTagInfo.java
+++ b/src/com/google/doclava/AttrTagInfo.java
@@ -22,6 +22,12 @@
 import java.util.regex.Matcher;
 
 public class AttrTagInfo extends TagInfo {
+  public static final AttrTagInfo[] EMPTY_ARRAY = new AttrTagInfo[0];
+
+  public static AttrTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new AttrTagInfo[size];
+  }
+
   private static final String REF_COMMAND = "ref";
   private static final String NAME_COMMAND = "name";
   private static final String DESCRIPTION_COMMAND = "description";
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index a5ee7d4..616cf23 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -2082,7 +2082,7 @@
         }
         if (mi == null) {
           Errors.error(Errors.REMOVED_METHOD, mInfo.position(), "Removed public method "
-              + mInfo.qualifiedName());
+              + mInfo.prettyQualifiedSignature());
           consistent = false;
         }
       }
@@ -2096,7 +2096,7 @@
         MethodInfo mi = ClassInfo.overriddenMethod(mInfo, this);
         if (mi == null) {
           Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public method "
-              + mInfo.qualifiedName());
+              + mInfo.prettyQualifiedSignature());
           consistent = false;
         }
       }
@@ -2109,14 +2109,14 @@
         }
       } else {
         Errors.error(Errors.REMOVED_METHOD, mInfo.position(), "Removed public constructor "
-            + mInfo.prettySignature());
+            + mInfo.prettyQualifiedSignature());
         consistent = false;
       }
     }
     for (MethodInfo mInfo : cl.mApiCheckConstructors.values()) {
       if (!mApiCheckConstructors.containsKey(mInfo.getHashableName())) {
         Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public constructor "
-            + mInfo.prettySignature());
+            + mInfo.prettyQualifiedSignature());
         consistent = false;
       }
     }
@@ -2201,7 +2201,7 @@
     if (!isDeprecated() == cl.isDeprecated()) {
       consistent = false;
       Errors.error(Errors.CHANGED_DEPRECATED, cl.position(), "Class " + cl.qualifiedName()
-          + " has changed deprecation state");
+          + " has changed deprecation state " + isDeprecated() + " --> " + cl.isDeprecated());
     }
 
     if (superclassName() != null) { // java.lang.Object can't have a superclass.
diff --git a/src/com/google/doclava/CodeTagInfo.java b/src/com/google/doclava/CodeTagInfo.java
index 1a4a864..56ffd13 100644
--- a/src/com/google/doclava/CodeTagInfo.java
+++ b/src/com/google/doclava/CodeTagInfo.java
@@ -17,6 +17,12 @@
 package com.google.doclava;
 
 public class CodeTagInfo extends TagInfo {
+  public static final CodeTagInfo[] EMPTY_ARRAY = new CodeTagInfo[0];
+
+  public static CodeTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new CodeTagInfo[size];
+  }
+
   private static String encode(String t) {
     t = t.replace("&", "&");
     t = t.replace("<", "&lt;");
diff --git a/src/com/google/doclava/Comment.java b/src/com/google/doclava/Comment.java
index cfb7aaf..33dee3c 100644
--- a/src/com/google/doclava/Comment.java
+++ b/src/com/google/doclava/Comment.java
@@ -19,12 +19,15 @@
 import java.util.regex.Pattern;
 import java.util.regex.Matcher;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 
 public class Comment {
   static final Pattern FIRST_SENTENCE =
       Pattern.compile("((.*?)\\.)[ \t\r\n\\<](.*)", Pattern.DOTALL);
 
-  private static final String[] KNOWN_TAGS = new String[] {
+  private static final Set<String> KNOWN_TAGS = new HashSet<String>(Arrays.asList(new String[] {
           "@author",
           "@since",
           "@version",
@@ -38,7 +41,7 @@
           "@sample",
           "@include",
           "@serial",
-      };
+      }));
 
   public Comment(String text, ContainerInfo base, SourcePositionInfo sp) {
     mText = text;
@@ -296,7 +299,14 @@
   }
 
   private boolean isWhitespaceChar(char c) {
-      return c == ' ' || c == '\r' || c == '\t' || c == '\n';
+      switch (c) {
+          case ' ':
+          case '\r':
+          case '\t':
+          case '\n':
+              return true;
+      }
+      return false;
   }
 
   private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) {
@@ -346,13 +356,7 @@
     } else if (name.equals("@include") || name.equals("@sample")) {
       mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
     } else {
-      boolean known = false;
-      for (String s : KNOWN_TAGS) {
-        if (s.equals(name)) {
-          known = true;
-          break;
-        }
-      }
+      boolean known = KNOWN_TAGS.contains(name);
       if (!known) {
         known = Doclava.knownTags.contains(name);
       }
@@ -416,7 +420,7 @@
         results.add(t);
       }
     }
-    return results.toArray(new TagInfo[results.size()]);
+    return results.toArray(TagInfo.getArray(results.size()));
   }
 
   public ParamTagInfo[] paramTags() {
@@ -516,18 +520,17 @@
     mText = null;
     mInitialized = true;
 
-    mInlineTags = mInlineTagsList.toArray(new TagInfo[mInlineTagsList.size()]);
-    mParamTags = mParamTagsList.toArray(new ParamTagInfo[mParamTagsList.size()]);
-    mSeeTags = mSeeTagsList.toArray(new SeeTagInfo[mSeeTagsList.size()]);
-    mThrowsTags = mThrowsTagsList.toArray(new ThrowsTagInfo[mThrowsTagsList.size()]);
-    mReturnTags =
-        ParsedTagInfo.joinTags(mReturnTagsList.toArray(new ParsedTagInfo[mReturnTagsList.size()]));
-    mDeprecatedTags =
-        ParsedTagInfo.joinTags(mDeprecatedTagsList.toArray(new ParsedTagInfo[mDeprecatedTagsList
-            .size()]));
-    mUndeprecateTags = mUndeprecateTagsList.toArray(new TagInfo[mUndeprecateTagsList.size()]);
-    mAttrTags = mAttrTagsList.toArray(new AttrTagInfo[mAttrTagsList.size()]);
-    mBriefTags = mBriefTagsList.toArray(new TagInfo[mBriefTagsList.size()]);
+    mInlineTags = mInlineTagsList.toArray(TagInfo.getArray(mInlineTagsList.size()));
+    mParamTags = mParamTagsList.toArray(ParamTagInfo.getArray(mParamTagsList.size()));
+    mSeeTags = mSeeTagsList.toArray(SeeTagInfo.getArray(mSeeTagsList.size()));
+    mThrowsTags = mThrowsTagsList.toArray(ThrowsTagInfo.getArray(mThrowsTagsList.size()));
+    mReturnTags = ParsedTagInfo.joinTags(
+        mReturnTagsList.toArray(ParsedTagInfo.getArray(mReturnTagsList.size())));
+    mDeprecatedTags = ParsedTagInfo.joinTags(
+        mDeprecatedTagsList.toArray(ParsedTagInfo.getArray(mDeprecatedTagsList.size())));
+    mUndeprecateTags = mUndeprecateTagsList.toArray(TagInfo.getArray(mUndeprecateTagsList.size()));
+    mAttrTags = mAttrTagsList.toArray(AttrTagInfo.getArray(mAttrTagsList.size()));
+    mBriefTags = mBriefTagsList.toArray(TagInfo.getArray(mBriefTagsList.size()));
 
     mParamTagsList = null;
     mSeeTagsList = null;
diff --git a/src/com/google/doclava/Converter.java b/src/com/google/doclava/Converter.java
index e620bf3..3153b41 100644
--- a/src/com/google/doclava/Converter.java
+++ b/src/com/google/doclava/Converter.java
@@ -107,12 +107,14 @@
     return (ClassInfo[]) mClasses.all();
   }
 
+  private static final MethodDoc[] EMPTY_METHOD_DOC = new MethodDoc[0];
+
   private static void initClass(ClassDoc c, ClassInfo cl) {
     MethodDoc[] annotationElements;
     if (c instanceof AnnotationTypeDoc) {
       annotationElements = ((AnnotationTypeDoc) c).elements();
     } else {
-      annotationElements = new MethodDoc[0];
+      annotationElements = EMPTY_METHOD_DOC;
     }
     cl.init(Converter.obtainType(c),
             new ArrayList<ClassInfo>(Arrays.asList(Converter.convertClasses(c.interfaces()))),
@@ -192,7 +194,7 @@
 
   public static TagInfo[] convertTags(Tag[] tags, ContainerInfo base) {
     int len = tags.length;
-    TagInfo[] out = new TagInfo[len];
+    TagInfo[] out = TagInfo.getArray(len);
     for (int i = 0; i < len; i++) {
       Tag t = tags[i];
       /*
diff --git a/src/com/google/doclava/Doclava.java b/src/com/google/doclava/Doclava.java
index 94331ee..e2f7d88 100644
--- a/src/com/google/doclava/Doclava.java
+++ b/src/com/google/doclava/Doclava.java
@@ -718,8 +718,9 @@
     }
 
     int i = 0;
-    for (String s : sorted.keySet()) {
-      PackageInfo pkg = sorted.get(s);
+    for (Map.Entry<String, PackageInfo> entry : sorted.entrySet()) {
+      String s = entry.getKey();
+      PackageInfo pkg = entry.getValue();
 
       if (pkg.isHiddenOrRemoved()) {
         continue;
diff --git a/src/com/google/doclava/FieldInfo.java b/src/com/google/doclava/FieldInfo.java
index ce80e9e..83d748a 100644
--- a/src/com/google/doclava/FieldInfo.java
+++ b/src/com/google/doclava/FieldInfo.java
@@ -423,7 +423,7 @@
     boolean consistent = true;
     if (!mType.equals(fInfo.mType)) {
       Errors.error(Errors.CHANGED_TYPE, fInfo.position(), "Field " + fInfo.qualifiedName()
-          + " has changed type");
+          + " has changed type from " + mType + " to " + fInfo.mType);
       consistent = false;
     } else if (!this.valueEquals(fInfo)) {
       Errors.error(Errors.CHANGED_VALUE, fInfo.position(), "Field " + fInfo.qualifiedName()
@@ -467,7 +467,7 @@
 
     if (isDeprecated() != fInfo.isDeprecated()) {
       Errors.error(Errors.CHANGED_DEPRECATED, fInfo.position(), "Field " + fInfo.qualifiedName()
-          + " has changed deprecation state");
+          + " has changed deprecation state " + isDeprecated() + " --> " + fInfo.isDeprecated());
       consistent = false;
     }
 
diff --git a/src/com/google/doclava/LiteralTagInfo.java b/src/com/google/doclava/LiteralTagInfo.java
index 1feb276..e6b9115 100644
--- a/src/com/google/doclava/LiteralTagInfo.java
+++ b/src/com/google/doclava/LiteralTagInfo.java
@@ -17,6 +17,12 @@
 package com.google.doclava;
 
 public class LiteralTagInfo extends TagInfo {
+  public static final LiteralTagInfo[] EMPTY_ARRAY = new LiteralTagInfo[0];
+
+  public static LiteralTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new LiteralTagInfo[size];
+  }
+
   private static String encode(String t) {
     t = t.replace("&", "&amp;");
     t = t.replace("<", "&lt;");
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
index f1659f3..5dc217c 100644
--- a/src/com/google/doclava/MethodInfo.java
+++ b/src/com/google/doclava/MethodInfo.java
@@ -333,6 +333,10 @@
   public String prettySignature() {
     return name() + prettyParameters();
   }
+
+  public String prettyQualifiedSignature() {
+    return qualifiedName() + prettyParameters();
+  }
   
   /**
    * Returns a printable version of the parameters of this method's signature.
@@ -405,7 +409,8 @@
               containingClass(), position()));
         }
       }
-      mThrowsTags = rv.toArray(new ThrowsTagInfo[rv.size()]);
+
+      mThrowsTags = rv.toArray(ThrowsTagInfo.getArray(rv.size()));
     }
     return mThrowsTags;
   }
@@ -424,6 +429,12 @@
     if (mParamTags == null) {
       final int N = mParameters.size();
 
+      if (N == 0) {
+          // Early out for empty case.
+          mParamTags = ParamTagInfo.EMPTY_ARRAY;
+          return ParamTagInfo.EMPTY_ARRAY;
+      }
+
       String[] names = new String[N];
       String[] comments = new String[N];
       SourcePositionInfo[] positions = new SourcePositionInfo[N];
@@ -464,7 +475,7 @@
       }
 
       // construct the results, and cache them for next time
-      mParamTags = new ParamTagInfo[N];
+      mParamTags = ParamTagInfo.getArray(N);
       for (i = 0; i < N; i++) {
         mParamTags[i] =
             new ParamTagInfo("@param", "@param", names[i] + " " + comments[i], parent(),
@@ -721,6 +732,8 @@
   public String qualifiedName() {
     String parentQName = (containingClass() != null)
         ? (containingClass().qualifiedName() + ".") : "";
+    // TODO: This logic doesn't work well with constructors, as name() for constructors already
+    // contains the containingClass's name, leading to things like A.B.B() being rendered as A.B.A.B()
     return parentQName + name();
   }
 
@@ -769,21 +782,22 @@
       }
 
       if (!consistent) {
-        Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method " + mInfo.qualifiedName()
-            + " has changed return type from " + mReturnType + " to " + mInfo.mReturnType);
+        Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method "
+            + mInfo.prettyQualifiedSignature() + " has changed return type from " + mReturnType
+            + " to " + mInfo.mReturnType);
       }
     }
 
     if (mIsAbstract != mInfo.mIsAbstract) {
       consistent = false;
-      Errors.error(Errors.CHANGED_ABSTRACT, mInfo.position(), "Method " + mInfo.qualifiedName()
-          + " has changed 'abstract' qualifier");
+      Errors.error(Errors.CHANGED_ABSTRACT, mInfo.position(), "Method "
+          + mInfo.prettyQualifiedSignature() + " has changed 'abstract' qualifier");
     }
 
     if (mIsNative != mInfo.mIsNative) {
       consistent = false;
-      Errors.error(Errors.CHANGED_NATIVE, mInfo.position(), "Method " + mInfo.qualifiedName()
-          + " has changed 'native' qualifier");
+      Errors.error(Errors.CHANGED_NATIVE, mInfo.position(), "Method "
+          + mInfo.prettyQualifiedSignature() + " has changed 'native' qualifier");
     }
 
     if (!mIsStatic) {
@@ -793,30 +807,32 @@
       // and (b) the method is not already inferred to be 'final' by virtue of its class.
       if (!isEffectivelyFinal() && mInfo.isEffectivelyFinal()) {
         consistent = false;
-        Errors.error(Errors.ADDED_FINAL, mInfo.position(), "Method " + mInfo.qualifiedName()
-            + " has added 'final' qualifier");
+        Errors.error(Errors.ADDED_FINAL, mInfo.position(), "Method "
+            + mInfo.prettyQualifiedSignature() + " has added 'final' qualifier");
       } else if (isEffectivelyFinal() && !mInfo.isEffectivelyFinal()) {
         consistent = false;
-        Errors.error(Errors.REMOVED_FINAL, mInfo.position(), "Method " + mInfo.qualifiedName()
-            + " has removed 'final' qualifier");
+        Errors.error(Errors.REMOVED_FINAL, mInfo.position(), "Method "
+            + mInfo.prettyQualifiedSignature() + " has removed 'final' qualifier");
       }
     }
 
     if (mIsStatic != mInfo.mIsStatic) {
       consistent = false;
-      Errors.error(Errors.CHANGED_STATIC, mInfo.position(), "Method " + mInfo.qualifiedName()
-          + " has changed 'static' qualifier");
+      Errors.error(Errors.CHANGED_STATIC, mInfo.position(), "Method "
+          + mInfo.prettyQualifiedSignature() + " has changed 'static' qualifier");
     }
 
     if (!scope().equals(mInfo.scope())) {
       consistent = false;
-      Errors.error(Errors.CHANGED_SCOPE, mInfo.position(), "Method " + mInfo.qualifiedName()
-          + " changed scope from " + scope() + " to " + mInfo.scope());
+      Errors.error(Errors.CHANGED_SCOPE, mInfo.position(), "Method "
+          + mInfo.prettyQualifiedSignature() + " changed scope from " + scope()
+          + " to " + mInfo.scope());
     }
 
     if (!isDeprecated() == mInfo.isDeprecated()) {
-      Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), "Method " + mInfo.qualifiedName()
-          + " has changed deprecation state " + isDeprecated() + " --> " + mInfo.isDeprecated());
+      Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), "Method "
+          + mInfo.prettyQualifiedSignature() + " has changed deprecation state " + isDeprecated()
+          + " --> " + mInfo.isDeprecated());
       consistent = false;
     }
 
@@ -835,8 +851,9 @@
       if (!mInfo.throwsException(exception)) {
         // exclude 'throws' changes to finalize() overrides with no arguments
         if (!name().equals("finalize") || (!mParameters.isEmpty())) {
-          Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Method " + mInfo.qualifiedName()
-              + " no longer throws exception " + exception.qualifiedName());
+          Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Method "
+              + mInfo.prettyQualifiedSignature() + " no longer throws exception "
+              + exception.qualifiedName());
           consistent = false;
         }
       }
@@ -846,8 +863,9 @@
       // exclude 'throws' changes to finalize() overrides with no arguments
       if (!throwsException(exec)) {
         if (!name().equals("finalize") || (!mParameters.isEmpty())) {
-          Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Method " + mInfo.qualifiedName()
-              + " added thrown exception " + exec.qualifiedName());
+          Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Method "
+              + mInfo.prettyQualifiedSignature() + " added thrown exception "
+              + exec.qualifiedName());
           consistent = false;
         }
       }
diff --git a/src/com/google/doclava/PackageInfo.java b/src/com/google/doclava/PackageInfo.java
index 02beaf7..c0f10da 100644
--- a/src/com/google/doclava/PackageInfo.java
+++ b/src/com/google/doclava/PackageInfo.java
@@ -394,9 +394,15 @@
     return mClasses;
   }
 
-  public boolean isConsistent(PackageInfo pInfo) {
+  public boolean isConsistent(PackageInfo pInfo, Collection<String> ignoredClasses) {
     boolean consistent = true;
     for (ClassInfo cInfo : mClasses.values()) {
+      // TODO: Add support for matching inner classes (e.g, something like
+      //  example.Type.* should match example.Type.InnerType)
+      if (ignoredClasses != null && ignoredClasses.contains(cInfo.qualifiedName())) {
+          // TODO: Log skipping this?
+          continue;
+      }
       if (pInfo.mClasses.containsKey(cInfo.name())) {
         if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()))) {
           consistent = false;
@@ -408,6 +414,10 @@
       }
     }
     for (ClassInfo cInfo : pInfo.mClasses.values()) {
+      if (ignoredClasses != null && ignoredClasses.contains(cInfo.qualifiedName())) {
+          // TODO: Log skipping this?
+          continue;
+      }
       if (!mClasses.containsKey(cInfo.name())) {
         Errors.error(Errors.ADDED_CLASS, cInfo.position(), "Added class " + cInfo.name()
             + " to package " + pInfo.name());
@@ -416,4 +426,8 @@
     }
     return consistent;
   }
+
+  public boolean isConsistent(PackageInfo pInfo) {
+    return isConsistent(pInfo, null);
+  }
 }
diff --git a/src/com/google/doclava/ParamTagInfo.java b/src/com/google/doclava/ParamTagInfo.java
index f8329bc..13eb30b 100644
--- a/src/com/google/doclava/ParamTagInfo.java
+++ b/src/com/google/doclava/ParamTagInfo.java
@@ -22,6 +22,12 @@
 import java.util.regex.Matcher;
 
 public class ParamTagInfo extends ParsedTagInfo {
+  public static final ParamTagInfo[] EMPTY_ARRAY = new ParamTagInfo[0];
+
+  public static ParamTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new ParamTagInfo[size];
+  }
+
   static final Pattern PATTERN = Pattern.compile("([^ \t\r\n]+)[ \t\r\n]+(.*)", Pattern.DOTALL);
 
   private boolean mIsTypeParameter;
diff --git a/src/com/google/doclava/ParsedTagInfo.java b/src/com/google/doclava/ParsedTagInfo.java
index bcb9230..aad3767 100755
--- a/src/com/google/doclava/ParsedTagInfo.java
+++ b/src/com/google/doclava/ParsedTagInfo.java
@@ -19,6 +19,12 @@
 import java.util.ArrayList;
 
 public class ParsedTagInfo extends TagInfo {
+  public static final ParsedTagInfo[] EMPTY_ARRAY = new ParsedTagInfo[0];
+
+  public static ParsedTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new ParsedTagInfo[size];
+  }
+
   private ContainerInfo mContainer;
   private String mCommentText;
   private Comment mComment;
@@ -50,6 +56,6 @@
         list.add(t[j]);
       }
     }
-    return list.toArray(new TagInfo[list.size()]);
+    return list.toArray(TagInfo.getArray(list.size()));
   }
 }
diff --git a/src/com/google/doclava/SampleTagInfo.java b/src/com/google/doclava/SampleTagInfo.java
index a69a3f3..66eb9ac 100644
--- a/src/com/google/doclava/SampleTagInfo.java
+++ b/src/com/google/doclava/SampleTagInfo.java
@@ -44,6 +44,12 @@
  * samples/ApiDemos/src/com/google/app/Notification1.java Bleh}
  */
 public class SampleTagInfo extends TagInfo {
+  public static final SampleTagInfo[] EMPTY_ARRAY = new SampleTagInfo[0];
+
+  public static SampleTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new SampleTagInfo[size];
+  }
+
   static final int STATE_BEGIN = 0;
   static final int STATE_MATCHING = 1;
 
diff --git a/src/com/google/doclava/SeeTagInfo.java b/src/com/google/doclava/SeeTagInfo.java
index 34e6aed..9f38655 100644
--- a/src/com/google/doclava/SeeTagInfo.java
+++ b/src/com/google/doclava/SeeTagInfo.java
@@ -19,6 +19,12 @@
 import com.google.clearsilver.jsilver.data.Data;
 
 public class SeeTagInfo extends TagInfo {
+  public static final SeeTagInfo[] EMPTY_ARRAY = new SeeTagInfo[0];
+
+  public static SeeTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new SeeTagInfo[size];
+  }
+
   private ContainerInfo mBase;
   LinkReference mLink;
 
diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java
index bc5e586..6157fa9 100644
--- a/src/com/google/doclava/Stubs.java
+++ b/src/com/google/doclava/Stubs.java
@@ -27,8 +27,10 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 public class Stubs {
   public static void writeStubsAndApi(String stubsDir, String apiFile, String keepListFile,
@@ -161,9 +163,10 @@
 
     // packages contains all the notStrippable classes mapped by their containing packages
     HashMap<PackageInfo, List<ClassInfo>> packages = new HashMap<PackageInfo, List<ClassInfo>>();
+    final HashSet<Pattern> stubPackageWildcards = extractWildcards(stubPackages);
     for (ClassInfo cl : notStrippable) {
       if (!cl.isDocOnly()) {
-        if (stubPackages == null || stubPackages.contains(cl.containingPackage().name())) {
+        if (shouldWriteStub(cl.containingPackage().name(), stubPackages, stubPackageWildcards)) {
           // write out the stubs
           if (stubsDir != null) {
             writeClassFile(stubsDir, notStrippable, cl);
@@ -211,6 +214,46 @@
     }
   }
 
+  private static boolean shouldWriteStub(final String packageName,
+          final HashSet<String> stubPackages, final HashSet<Pattern> stubPackageWildcards) {
+    if (stubPackages == null) {
+      // There aren't any stub packages set, write all stubs
+      return true;
+    }
+    if (stubPackages.contains(packageName)) {
+      // Stub packages contains package, return true
+      return true;
+    }
+    if (stubPackageWildcards != null) {
+      // Else, we will iterate through the wildcards to see if there's a match
+      for (Pattern wildcard : stubPackageWildcards) {
+        if (wildcard.matcher(packageName).matches()) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private static HashSet<Pattern> extractWildcards(HashSet<String> stubPackages) {
+    HashSet<Pattern> wildcards = null;
+    if (stubPackages != null) {
+      for (Iterator<String> i = stubPackages.iterator(); i.hasNext();) {
+        final String pkg = i.next();
+        if (pkg.indexOf('*') != -1) {
+          if (wildcards == null) {
+            wildcards = new HashSet<Pattern>();
+          }
+          // Add the compiled wildcard, replacing * with the regex equivalent
+          wildcards.add(Pattern.compile(pkg.replace("*", ".*?")));
+          // And remove the raw wildcard from the packages
+          i.remove();
+        }
+      }
+    }
+    return wildcards;
+  }
+
   private static ClassInfo findHiddenClasses(TypeInfo ti) {
     ClassInfo ci = ti.asClassInfo();
     if (ci == null) return null;
@@ -1057,7 +1100,7 @@
     returnString = returnString.replaceAll("<", "&lt;");
     returnString = returnString.replaceAll(">", "&gt;");
     returnString = returnString.replaceAll("\"", "&quot;");
-    returnString = returnString.replaceAll("'", "&pos;");
+    returnString = returnString.replaceAll("'", "&apos;");
     return returnString;
   }
 
diff --git a/src/com/google/doclava/TagInfo.java b/src/com/google/doclava/TagInfo.java
index ec1f811..7bb640e 100644
--- a/src/com/google/doclava/TagInfo.java
+++ b/src/com/google/doclava/TagInfo.java
@@ -19,6 +19,12 @@
 import com.google.clearsilver.jsilver.data.Data;
 
 public class TagInfo {
+  public static final TagInfo[] EMPTY_ARRAY = new TagInfo[0];
+
+  public static TagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new TagInfo[size];
+  }
+
   private String mName;
   private String mText;
   private String mKind;
diff --git a/src/com/google/doclava/TextTagInfo.java b/src/com/google/doclava/TextTagInfo.java
index 35a486b..7403883 100644
--- a/src/com/google/doclava/TextTagInfo.java
+++ b/src/com/google/doclava/TextTagInfo.java
@@ -17,6 +17,12 @@
 package com.google.doclava;
 
 public class TextTagInfo extends TagInfo {
+  public static final TextTagInfo[] EMPTY_ARRAY = new TextTagInfo[0];
+
+  public static TextTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new TextTagInfo[size];
+  }
+
   TextTagInfo(String n, String k, String t, SourcePositionInfo p) {
     super(n, k, Doclava.escape(t), p);
   }
diff --git a/src/com/google/doclava/ThrowsTagInfo.java b/src/com/google/doclava/ThrowsTagInfo.java
index 5f49485..9a9e72f 100644
--- a/src/com/google/doclava/ThrowsTagInfo.java
+++ b/src/com/google/doclava/ThrowsTagInfo.java
@@ -22,7 +22,14 @@
 import java.util.regex.Matcher;
 
 public class ThrowsTagInfo extends ParsedTagInfo {
+  public static final ThrowsTagInfo[] EMPTY_ARRAY = new ThrowsTagInfo[0];
+
+  public static ThrowsTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new ThrowsTagInfo[size];
+  }
+
   static final Pattern PATTERN = Pattern.compile("(\\S+)\\s+(.*)", Pattern.DOTALL);
+
   private ClassInfo mException;
 
   public ThrowsTagInfo(String name, String kind, String text, ContainerInfo base,
diff --git a/src/com/google/doclava/apicheck/ApiCheck.java b/src/com/google/doclava/apicheck/ApiCheck.java
index 28d7ce0..9698c89 100644
--- a/src/com/google/doclava/apicheck/ApiCheck.java
+++ b/src/com/google/doclava/apicheck/ApiCheck.java
@@ -23,6 +23,7 @@
 import java.io.PrintStream;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.Stack;
 
@@ -39,7 +40,8 @@
     for (i = 0; i < allArgs.size(); i++) {
       // flags with one value attached
       String flag = allArgs.get(i);
-      if (flag.equals("-error") || flag.equals("-warning") || flag.equals("-hide")) {
+      if (flag.equals("-error") || flag.equals("-warning") || flag.equals("-hide")
+          || flag.equals("-ignoreClass") || flag.equals("-ignorePackage")) {
         String[] arg = new String[2];
         arg[0] = flag;
         arg[1] = allArgs.get(++i);
@@ -81,6 +83,11 @@
       args.add(a);
     }
 
+    // Not having having any classes or packages ignored is the common case.
+    // Avoid a hashCode call in a common loop by not passing in a HashSet in this case.
+    Set<String> ignoredPackages = null;
+    Set<String> ignoredClasses = null;
+
     ArrayList<String[]> flags = ApiCheck.parseFlags(args);
     for (String[] a : flags) {
       if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) {
@@ -98,6 +105,16 @@
           System.err.println("Bad argument: " + a[0] + " " + a[1]);
           return new Report(2, Errors.getErrors());
         }
+      } else if (a[0].equals("-ignoreClass")) {
+        if (ignoredClasses == null) {
+          ignoredClasses = new HashSet<String>();
+        }
+        ignoredClasses.add(a[1]);
+      } else if (a[0].equals("-ignorePackage")) {
+        if (ignoredPackages == null) {
+          ignoredPackages = new HashSet<String>();
+        }
+        ignoredPackages.add(a[1]);
       }
     }
 
@@ -121,11 +138,11 @@
 
     // only run the consistency check if we haven't had XML parse errors
     if (!Errors.hadError) {
-      oldApi.isConsistent(newApi);
+      oldApi.isConsistent(newApi, ignoredPackages, ignoredClasses);
     }
 
     if (!Errors.hadError) {
-      oldRemovedApi.isConsistent(newRemovedApi);
+      oldRemovedApi.isConsistent(newRemovedApi, ignoredPackages, ignoredClasses);
     }
 
     return new Report(Errors.hadError ? 1 : 0, Errors.getErrors());
diff --git a/src/com/google/doclava/apicheck/ApiInfo.java b/src/com/google/doclava/apicheck/ApiInfo.java
index 711a9f4..2752f3a 100644
--- a/src/com/google/doclava/apicheck/ApiInfo.java
+++ b/src/com/google/doclava/apicheck/ApiInfo.java
@@ -20,6 +20,7 @@
 import com.google.doclava.Errors;
 import com.google.doclava.PackageInfo;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -58,12 +59,25 @@
 
   /**
    * Checks to see if this api is consistent with a newer version.
+   *
+   * @param otherApi the other api to test consistency against
+   * @param ignoredPackages packages to skip consistency checks (will match by exact name)
+   * @param ignoredClasses classes to skip consistency checks (will match by exact fully qualified
+   * name)
    */
-  public boolean isConsistent(ApiInfo otherApi) {
+  public boolean isConsistent(ApiInfo otherApi,
+      Collection<String> ignoredPackages, Collection<String> ignoredClasses) {
     boolean consistent = true;
     for (PackageInfo pInfo : mPackages.values()) {
+      // TODO: Add support for matching subpackages (e.g, something like
+      // test.example.* should match test.example.subpackage, and
+      // test.example.** should match the above AND test.example.subpackage.more)
+      if (ignoredPackages != null && ignoredPackages.contains(pInfo.name())) {
+          // TODO: Log skipping this?
+          continue;
+      }
       if (otherApi.getPackages().containsKey(pInfo.name())) {
-        if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()))) {
+        if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()), ignoredClasses)) {
           consistent = false;
         }
       } else {
@@ -72,6 +86,10 @@
       }
     }
     for (PackageInfo pInfo : otherApi.mPackages.values()) {
+      if (ignoredPackages != null && ignoredPackages.contains(pInfo.name())) {
+          // TODO: Log skipping this?
+          continue;
+      }
       if (!mPackages.containsKey(pInfo.name())) {
         Errors.error(Errors.ADDED_PACKAGE, pInfo.position(), "Added package " + pInfo.name());
         consistent = false;
@@ -80,6 +98,13 @@
     return consistent;
   }
 
+  /**
+   * Checks to see if this api is consistent with a newer version.
+   */
+  public boolean isConsistent(ApiInfo otherApi) {
+    return isConsistent(otherApi, null, null);
+  }
+
   public HashMap<String, PackageInfo> getPackages() {
     return mPackages;
   }