Initial import of Doclava project

Change-Id: Ia5ae56f1700fce98e0ae6954fa2df617ec0537bb
diff --git a/src/com/google/doclava/AnnotationInstanceInfo.java b/src/com/google/doclava/AnnotationInstanceInfo.java
new file mode 100644
index 0000000..bb498d7
--- /dev/null
+++ b/src/com/google/doclava/AnnotationInstanceInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class AnnotationInstanceInfo {
+  private ClassInfo mType;
+  private AnnotationValueInfo[] mElementValues;
+
+  public AnnotationInstanceInfo(ClassInfo type, AnnotationValueInfo[] elementValues) {
+    mType = type;
+    mElementValues = elementValues;
+  }
+
+  ClassInfo type() {
+    return mType;
+  }
+
+  AnnotationValueInfo[] elementValues() {
+    return mElementValues;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder str = new StringBuilder();
+    str.append("@");
+    str.append(mType.qualifiedName());
+    str.append("(");
+    AnnotationValueInfo[] values = mElementValues;
+    final int N = values.length;
+    for (int i = 0; i < N; i++) {
+      AnnotationValueInfo value = values[i];
+      str.append(value.element().name());
+      str.append("=");
+      str.append(value.valueString());
+      if (i != N - 1) {
+        str.append(",");
+      }
+    }
+    str.append(")");
+    return str.toString();
+  }
+}
diff --git a/src/com/google/doclava/AnnotationValueInfo.java b/src/com/google/doclava/AnnotationValueInfo.java
new file mode 100644
index 0000000..2022a59
--- /dev/null
+++ b/src/com/google/doclava/AnnotationValueInfo.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class AnnotationValueInfo {
+  private Object mValue;
+  private String mString;
+  private MethodInfo mElement;
+
+  public AnnotationValueInfo(MethodInfo element) {
+    mElement = element;
+  }
+
+  public void init(Object value) {
+    mValue = value;
+  }
+
+  public MethodInfo element() {
+    return mElement;
+  }
+
+  public Object value() {
+    return mValue;
+  }
+
+  public String valueString() {
+    Object v = mValue;
+    if (v instanceof TypeInfo) {
+      return ((TypeInfo) v).fullName();
+    } else if (v instanceof FieldInfo) {
+      StringBuilder str = new StringBuilder();
+      FieldInfo f = (FieldInfo) v;
+      str.append(f.containingClass().qualifiedName());
+      str.append('.');
+      str.append(f.name());
+      return str.toString();
+    } else if (v instanceof AnnotationInstanceInfo) {
+      return v.toString();
+    } else if (v instanceof AnnotationValueInfo[]) {
+      StringBuilder str = new StringBuilder();
+      AnnotationValueInfo[] array = (AnnotationValueInfo[]) v;
+      final int N = array.length;
+      str.append("{");
+      for (int i = 0; i < array.length; i++) {
+        str.append(array[i].valueString());
+        if (i != N - 1) {
+          str.append(",");
+        }
+      }
+      str.append("}");
+      return str.toString();
+    } else {
+      return FieldInfo.constantLiteralValue(v);
+    }
+  }
+}
diff --git a/src/com/google/doclava/AttrTagInfo.java b/src/com/google/doclava/AttrTagInfo.java
new file mode 100644
index 0000000..909cacf
--- /dev/null
+++ b/src/com/google/doclava/AttrTagInfo.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+public class AttrTagInfo extends TagInfo {
+  private static final String REF_COMMAND = "ref";
+  private static final String NAME_COMMAND = "name";
+  private static final String DESCRIPTION_COMMAND = "description";
+  private static final Pattern TEXT = Pattern.compile("(\\S+)\\s*(.*)", Pattern.DOTALL);
+  private static final Pattern NAME_TEXT = Pattern.compile("(\\S+)(.*)", Pattern.DOTALL);
+
+  private ContainerInfo mBase;
+  private String mCommand;
+
+  // if mCommand == "ref"
+  private FieldInfo mRefField;
+  private AttributeInfo mAttrInfo;
+
+  // if mCommand == "name"
+  private String mAttrName;
+
+  // if mCommand == "description"
+  private Comment mDescrComment;
+
+  AttrTagInfo(String name, String kind, String text, ContainerInfo base, SourcePositionInfo position) {
+    super(name, kind, text, position);
+    mBase = base;
+
+    parse(text, base, position);
+  }
+
+  void parse(String text, ContainerInfo base, SourcePositionInfo position) {
+    Matcher m;
+
+    m = TEXT.matcher(text);
+    if (!m.matches()) {
+      Errors.error(Errors.BAD_ATTR_TAG, position, "Bad @attr tag: " + text);
+      return;
+    }
+
+    String command = m.group(1);
+    String more = m.group(2);
+
+    if (REF_COMMAND.equals(command)) {
+      String ref = more.trim();
+      LinkReference linkRef = LinkReference.parse(ref, mBase, position, false);
+      if (!linkRef.good) {
+        Errors.error(Errors.BAD_ATTR_TAG, position, "Unresolved @attr ref: " + ref);
+        return;
+      }
+      if (!(linkRef.memberInfo instanceof FieldInfo)) {
+        Errors.error(Errors.BAD_ATTR_TAG, position, "@attr must be a field: " + ref);
+        return;
+      }
+      mCommand = command;
+      mRefField = (FieldInfo) linkRef.memberInfo;
+    } else if (NAME_COMMAND.equals(command)) {
+      m = NAME_TEXT.matcher(more);
+      if (!m.matches() || m.group(2).trim().length() != 0) {
+        Errors.error(Errors.BAD_ATTR_TAG, position, "Bad @attr name tag: " + more);
+        return;
+      }
+      mCommand = command;
+      mAttrName = m.group(1);
+    } else if (DESCRIPTION_COMMAND.equals(command)) {
+      mCommand = command;
+      mDescrComment = new Comment(more, base, position);
+    } else {
+      Errors.error(Errors.BAD_ATTR_TAG, position, "Bad @attr command: " + command);
+    }
+  }
+
+  public FieldInfo reference() {
+    return REF_COMMAND.equals(mCommand) ? mRefField : null;
+  }
+
+  @Override
+  public String name() {
+    return NAME_COMMAND.equals(mCommand) ? mAttrName : null;
+  }
+
+  public Comment description() {
+    return DESCRIPTION_COMMAND.equals(mCommand) ? mDescrComment : null;
+  }
+
+  @Override
+  public void makeHDF(Data data, String base) {
+    super.makeHDF(data, base);
+  }
+
+  public void setAttribute(AttributeInfo info) {
+    mAttrInfo = info;
+  }
+
+  public static void makeReferenceHDF(Data data, String base, AttrTagInfo[] tags) {
+    int i = 0;
+    for (AttrTagInfo t : tags) {
+      if (REF_COMMAND.equals(t.mCommand)) {
+        if (t.mAttrInfo == null) {
+          String msg = "ERROR: unlinked attr: " + t.mRefField.name();
+          if (false) {
+            System.out.println(msg);
+          } else {
+            throw new RuntimeException(msg);
+          }
+        } else {
+          data.setValue(base + "." + i + ".name", t.mAttrInfo.name());
+          data.setValue(base + "." + i + ".href", t.mAttrInfo.htmlPage());
+          i++;
+        }
+      }
+    }
+  }
+
+}
diff --git a/src/com/google/doclava/AttributeInfo.java b/src/com/google/doclava/AttributeInfo.java
new file mode 100644
index 0000000..25d424f
--- /dev/null
+++ b/src/com/google/doclava/AttributeInfo.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+public class AttributeInfo {
+  public static final Comparator<AttributeInfo> comparator = new Comparator<AttributeInfo>() {
+    public int compare(AttributeInfo a, AttributeInfo b) {
+      return a.name().compareTo(b.name());
+    }
+  };
+
+  public FieldInfo attrField;
+  public ArrayList<MethodInfo> methods = new ArrayList<MethodInfo>();
+
+  private ClassInfo mClass;
+  private String mName;
+  private Comment mComment;
+
+  public AttributeInfo(ClassInfo cl, FieldInfo f) {
+    mClass = cl;
+    attrField = f;
+  }
+
+  public String name() {
+    if (mName == null) {
+      for (AttrTagInfo comment : attrField.comment().attrTags()) {
+        String n = comment.name();
+        if (n != null) {
+          mName = n;
+          return n;
+        }
+      }
+    }
+    return mName;
+  }
+
+  public Comment comment() {
+    if (mComment == null) {
+      for (AttrTagInfo attr : attrField.comment().attrTags()) {
+        Comment c = attr.description();
+        if (c != null) {
+          mComment = c;
+          return c;
+        }
+      }
+    }
+    if (mComment == null) {
+      return new Comment("", mClass, SourcePositionInfo.UNKNOWN);
+    }
+    return mComment;
+  }
+
+  public String anchor() {
+    return "attr_" + name();
+  }
+
+  public String htmlPage() {
+    return mClass.htmlPage() + "#" + anchor();
+  }
+
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".name", name());
+    data.setValue(base + ".anchor", anchor());
+    data.setValue(base + ".href", htmlPage());
+    data.setValue(base + ".R.name", attrField.name());
+    data.setValue(base + ".R.href", attrField.htmlPage());
+    TagInfo.makeHDF(data, base + ".deprecated", attrField.comment().deprecatedTags());
+    TagInfo.makeHDF(data, base + ".shortDescr", comment().briefTags());
+    TagInfo.makeHDF(data, base + ".descr", comment().tags());
+
+    int i = 0;
+    for (MethodInfo m : methods) {
+      String s = base + ".methods." + i;
+      data.setValue(s + ".href", m.htmlPage());
+      data.setValue(s + ".name", m.prettySignature());
+    }
+  }
+
+  public boolean checkLevel() {
+    return attrField.checkLevel();
+  }
+}
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
new file mode 100644
index 0000000..77863fd
--- /dev/null
+++ b/src/com/google/doclava/ClassInfo.java
@@ -0,0 +1,1716 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+import com.google.doclava.apicheck.ApiInfo;
+
+import com.sun.javadoc.*;
+import java.util.*;
+
+public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Scoped {
+  public static final Comparator<ClassInfo> comparator = new Comparator<ClassInfo>() {
+    public int compare(ClassInfo a, ClassInfo b) {
+      return a.name().compareTo(b.name());
+    }
+  };
+
+  public static final Comparator<ClassInfo> qualifiedComparator = new Comparator<ClassInfo>() {
+    public int compare(ClassInfo a, ClassInfo b) {
+      return a.qualifiedName().compareTo(b.qualifiedName());
+    }
+  };
+  
+  /**
+   * Constructs a stub representation of a class.
+   */
+  public ClassInfo(String qualifiedName) {
+    super("", SourcePositionInfo.UNKNOWN);
+    
+    mQualifiedName = qualifiedName;
+    if (qualifiedName.lastIndexOf('.') != -1) {
+      mName = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
+    } else {
+      mName = qualifiedName;
+    }
+  }
+
+  public ClassInfo(ClassDoc cl, String rawCommentText, SourcePositionInfo position,
+      boolean isPublic, boolean isProtected, boolean isPackagePrivate, boolean isPrivate,
+      boolean isStatic, boolean isInterface, boolean isAbstract, boolean isOrdinaryClass,
+      boolean isException, boolean isError, boolean isEnum, boolean isAnnotation, boolean isFinal,
+      boolean isIncluded, String name, String qualifiedName, String qualifiedTypeName,
+      boolean isPrimitive) {
+    super(rawCommentText, position);
+
+    mClass = cl;
+    mIsPublic = isPublic;
+    mIsProtected = isProtected;
+    mIsPackagePrivate = isPackagePrivate;
+    mIsPrivate = isPrivate;
+    mIsStatic = isStatic;
+    mIsInterface = isInterface;
+    mIsAbstract = isAbstract;
+    mIsOrdinaryClass = isOrdinaryClass;
+    mIsException = isException;
+    mIsError = isError;
+    mIsEnum = isEnum;
+    mIsAnnotation = isAnnotation;
+    mIsFinal = isFinal;
+    mIsIncluded = isIncluded;
+    mName = name;
+    mQualifiedName = qualifiedName;
+    mQualifiedTypeName = qualifiedTypeName;
+    mIsPrimitive = isPrimitive;
+    mNameParts = name.split("\\.");
+  }
+
+  public void init(TypeInfo typeInfo, ClassInfo[] interfaces, TypeInfo[] interfaceTypes,
+      ClassInfo[] innerClasses, MethodInfo[] constructors, MethodInfo[] methods,
+      MethodInfo[] annotationElements, FieldInfo[] fields, FieldInfo[] enumConstants,
+      PackageInfo containingPackage, ClassInfo containingClass, ClassInfo superclass,
+      TypeInfo superclassType, AnnotationInstanceInfo[] annotations) {
+    mTypeInfo = typeInfo;
+    mRealInterfaces = new ArrayList<ClassInfo>();
+    for (ClassInfo cl : interfaces) {
+      mRealInterfaces.add(cl);
+    }
+    mRealInterfaceTypes = interfaceTypes;
+    mInnerClasses = innerClasses;
+    mAllConstructors = constructors;
+    mAllSelfMethods = methods;
+    mAnnotationElements = annotationElements;
+    mAllSelfFields = fields;
+    mEnumConstants = enumConstants;
+    mContainingPackage = containingPackage;
+    mContainingClass = containingClass;
+    mRealSuperclass = superclass;
+    mRealSuperclassType = superclassType;
+    mAnnotations = annotations;
+
+    // after providing new methods and new superclass info,clear any cached
+    // lists of self + superclass methods, ctors, etc.
+    mSuperclassInit = false;
+    mConstructors = null;
+    mMethods = null;
+    mSelfMethods = null;
+    mFields = null;
+    mSelfFields = null;
+    mSelfAttributes = null;
+    mDeprecatedKnown = false;
+
+    Arrays.sort(mEnumConstants, FieldInfo.comparator);
+    Arrays.sort(mInnerClasses, ClassInfo.comparator);
+  }
+
+  public void init2() {
+    // calling this here forces the AttrTagInfo objects to be linked to the AttribtueInfo
+    // objects
+    selfAttributes();
+  }
+
+  public void init3(TypeInfo[] types, ClassInfo[] realInnerClasses) {
+    mTypeParameters = types;
+    mRealInnerClasses = realInnerClasses;
+  }
+
+  public ClassInfo[] getRealInnerClasses() {
+    return mRealInnerClasses;
+  }
+
+  public TypeInfo[] getTypeParameters() {
+    return mTypeParameters;
+  }
+
+  public boolean checkLevel() {
+    int val = mCheckLevel;
+    if (val >= 0) {
+      return val != 0;
+    } else {
+      boolean v =
+          Doclava.checkLevel(mIsPublic, mIsProtected, mIsPackagePrivate, mIsPrivate, isHidden());
+      mCheckLevel = v ? 1 : 0;
+      return v;
+    }
+  }
+
+  public int compareTo(Object that) {
+    if (that instanceof ClassInfo) {
+      return mQualifiedName.compareTo(((ClassInfo) that).mQualifiedName);
+    } else {
+      return this.hashCode() - that.hashCode();
+    }
+  }
+
+  @Override
+  public ContainerInfo parent() {
+    return this;
+  }
+
+  public boolean isPublic() {
+    return mIsPublic;
+  }
+
+  public boolean isProtected() {
+    return mIsProtected;
+  }
+
+  public boolean isPackagePrivate() {
+    return mIsPackagePrivate;
+  }
+
+  public boolean isPrivate() {
+    return mIsPrivate;
+  }
+
+  public boolean isStatic() {
+    return mIsStatic;
+  }
+
+  public boolean isInterface() {
+    return mIsInterface;
+  }
+
+  public boolean isAbstract() {
+    return mIsAbstract;
+  }
+
+  public PackageInfo containingPackage() {
+    return mContainingPackage;
+  }
+
+  public ClassInfo containingClass() {
+    return mContainingClass;
+  }
+
+  public boolean isOrdinaryClass() {
+    return mIsOrdinaryClass;
+  }
+
+  public boolean isException() {
+    return mIsException;
+  }
+
+  public boolean isError() {
+    return mIsError;
+  }
+
+  public boolean isEnum() {
+    return mIsEnum;
+  }
+
+  public boolean isAnnotation() {
+    return mIsAnnotation;
+  }
+
+  public boolean isFinal() {
+    return mIsFinal;
+  }
+
+  public boolean isIncluded() {
+    return mIsIncluded;
+  }
+
+  public HashSet<String> typeVariables() {
+    HashSet<String> result = TypeInfo.typeVariables(mTypeInfo.typeArguments());
+    ClassInfo cl = containingClass();
+    while (cl != null) {
+      TypeInfo[] types = cl.asTypeInfo().typeArguments();
+      if (types != null) {
+        TypeInfo.typeVariables(types, result);
+      }
+      cl = cl.containingClass();
+    }
+    return result;
+  }
+
+  private static void gatherHiddenInterfaces(ClassInfo cl, HashSet<ClassInfo> interfaces) {
+    for (ClassInfo iface : cl.mRealInterfaces) {
+      if (iface.checkLevel()) {
+        interfaces.add(iface);
+      } else {
+        gatherHiddenInterfaces(iface, interfaces);
+      }
+    }
+  }
+
+  public ClassInfo[] interfaces() {
+    if (mInterfaces == null) {
+      if (checkLevel()) {
+        HashSet<ClassInfo> interfaces = new HashSet<ClassInfo>();
+        ClassInfo superclass = mRealSuperclass;
+        while (superclass != null && !superclass.checkLevel()) {
+          gatherHiddenInterfaces(superclass, interfaces);
+          superclass = superclass.mRealSuperclass;
+        }
+        gatherHiddenInterfaces(this, interfaces);
+        mInterfaces = interfaces.toArray(new ClassInfo[interfaces.size()]);
+      } else {
+        // put something here in case someone uses it
+        mInterfaces = new ClassInfo[mRealInterfaces.size()];
+        mRealInterfaces.toArray(mInterfaces);
+      }
+      Arrays.sort(mInterfaces, ClassInfo.qualifiedComparator);
+    }
+    return mInterfaces;
+  }
+
+  public ClassInfo[] realInterfaces() {
+    ClassInfo[] classInfos = new ClassInfo[mRealInterfaces.size()];
+    return mRealInterfaces.toArray(classInfos);
+  }
+
+  TypeInfo[] realInterfaceTypes() {
+    return mRealInterfaceTypes;
+  }
+
+  public String name() {
+    return mName;
+  }
+
+  public String[] nameParts() {
+    return mNameParts;
+  }
+
+  public String leafName() {
+    return mNameParts[mNameParts.length - 1];
+  }
+
+  public String qualifiedName() {
+    return mQualifiedName;
+  }
+
+  public String qualifiedTypeName() {
+    return mQualifiedTypeName;
+  }
+
+  public boolean isPrimitive() {
+    return mIsPrimitive;
+  }
+
+  public MethodInfo[] allConstructors() {
+    return mAllConstructors;
+  }
+
+  public MethodInfo[] constructors() {
+    if (mConstructors == null) {
+      MethodInfo[] methods = mAllConstructors;
+      ArrayList<MethodInfo> ctors = new ArrayList<MethodInfo>();
+      for (int i = 0; i < methods.length; i++) {
+        MethodInfo m = methods[i];
+        if (!m.isHidden()) {
+          ctors.add(m);
+        }
+      }
+      mConstructors = ctors.toArray(new MethodInfo[ctors.size()]);
+      Arrays.sort(mConstructors, MethodInfo.comparator);
+    }
+    return mConstructors;
+  }
+
+  public ClassInfo[] innerClasses() {
+    return mInnerClasses;
+  }
+
+  public TagInfo[] inlineTags() {
+    return comment().tags();
+  }
+
+  public TagInfo[] firstSentenceTags() {
+    return comment().briefTags();
+  }
+
+  public boolean isDeprecated() {
+    boolean deprecated = false;
+    if (!mDeprecatedKnown) {
+      boolean commentDeprecated = comment().isDeprecated();
+      boolean annotationDeprecated = false;
+      for (AnnotationInstanceInfo annotation : annotations()) {
+        if (annotation.type().qualifiedName().equals("java.lang.Deprecated")) {
+          annotationDeprecated = true;
+          break;
+        }
+      }
+
+      if (commentDeprecated != annotationDeprecated) {
+        Errors.error(Errors.DEPRECATION_MISMATCH, position(), "Class " + qualifiedName()
+            + ": @Deprecated annotation and @deprecated comment do not match");
+      }
+
+      mIsDeprecated = commentDeprecated | annotationDeprecated;
+      mDeprecatedKnown = true;
+    }
+    return mIsDeprecated;
+  }
+
+  public TagInfo[] deprecatedTags() {
+    // Should we also do the interfaces?
+    return comment().deprecatedTags();
+  }
+
+  public MethodInfo[] methods() {
+    if (mMethods == null) {
+      TreeMap<String, MethodInfo> all = new TreeMap<String, MethodInfo>();
+
+      ClassInfo[] ifaces = interfaces();
+      for (ClassInfo iface : ifaces) {
+        if (iface != null) {
+          MethodInfo[] inhereted = iface.methods();
+          for (MethodInfo method : inhereted) {
+            String key = method.getHashableName();
+            all.put(key, method);
+          }
+        }
+      }
+
+      ClassInfo superclass = superclass();
+      if (superclass != null) {
+        MethodInfo[] inhereted = superclass.methods();
+        for (MethodInfo method : inhereted) {
+          String key = method.getHashableName();
+          all.put(key, method);
+        }
+      }
+
+      MethodInfo[] methods = selfMethods();
+      for (MethodInfo method : methods) {
+        String key = method.getHashableName();
+        all.put(key, method);
+      }
+
+      mMethods = all.values().toArray(new MethodInfo[all.size()]);
+      Arrays.sort(mMethods, MethodInfo.comparator);
+    }
+    return mMethods;
+  }
+
+  public MethodInfo[] annotationElements() {
+    return mAnnotationElements;
+  }
+
+  public AnnotationInstanceInfo[] annotations() {
+    return mAnnotations;
+  }
+
+  private static void addFields(ClassInfo cl, TreeMap<String, FieldInfo> all) {
+    FieldInfo[] fields = cl.fields();
+    int N = fields.length;
+    for (int i = 0; i < N; i++) {
+      FieldInfo f = fields[i];
+      all.put(f.name(), f);
+    }
+  }
+
+  public FieldInfo[] fields() {
+    if (mFields == null) {
+      int N;
+      TreeMap<String, FieldInfo> all = new TreeMap<String, FieldInfo>();
+
+      ClassInfo[] interfaces = interfaces();
+      N = interfaces.length;
+      for (int i = 0; i < N; i++) {
+        addFields(interfaces[i], all);
+      }
+
+      ClassInfo superclass = superclass();
+      if (superclass != null) {
+        addFields(superclass, all);
+      }
+
+      FieldInfo[] fields = selfFields();
+      N = fields.length;
+      for (int i = 0; i < N; i++) {
+        FieldInfo f = fields[i];
+        if (!f.isHidden()) {
+          String key = f.name();
+          all.put(key, f);
+        }
+      }
+
+      mFields = all.values().toArray(new FieldInfo[0]);
+    }
+    return mFields;
+  }
+
+  public void gatherFields(ClassInfo owner, ClassInfo cl, HashMap<String, FieldInfo> fields) {
+    FieldInfo[] flds = cl.selfFields();
+    for (FieldInfo f : flds) {
+      if (f.checkLevel()) {
+        fields.put(f.name(), f.cloneForClass(owner));
+      }
+    }
+  }
+
+  public FieldInfo[] selfFields() {
+    if (mSelfFields == null) {
+      HashMap<String, FieldInfo> fields = new HashMap<String, FieldInfo>();
+      // our hidden parents
+      if (mRealSuperclass != null && !mRealSuperclass.checkLevel()) {
+        gatherFields(this, mRealSuperclass, fields);
+      }
+      for (ClassInfo iface : mRealInterfaces) {
+        if (!iface.checkLevel()) {
+          gatherFields(this, iface, fields);
+        }
+      }
+      // mine
+      FieldInfo[] selfFields = mAllSelfFields;
+      for (int i = 0; i < selfFields.length; i++) {
+        FieldInfo f = selfFields[i];
+        if (!f.isHidden()) {
+          fields.put(f.name(), f);
+        }
+      }
+      // combine and return in
+      mSelfFields = fields.values().toArray(new FieldInfo[fields.size()]);
+      Arrays.sort(mSelfFields, FieldInfo.comparator);
+    }
+    return mSelfFields;
+  }
+
+  public FieldInfo[] allSelfFields() {
+    return mAllSelfFields;
+  }
+
+  private void gatherMethods(ClassInfo owner, ClassInfo cl, HashMap<String, MethodInfo> methods) {
+    MethodInfo[] meth = cl.selfMethods();
+    for (MethodInfo m : meth) {
+      if (m.checkLevel()) {
+        methods.put(m.name() + m.signature(), m.cloneForClass(owner));
+      }
+    }
+  }
+
+  public MethodInfo[] selfMethods() {
+    if (mSelfMethods == null) {
+      HashMap<String, MethodInfo> methods = new HashMap<String, MethodInfo>();
+      // our hidden parents
+      if (mRealSuperclass != null && !mRealSuperclass.checkLevel()) {
+        gatherMethods(this, mRealSuperclass, methods);
+      }
+      for (ClassInfo iface : mRealInterfaces) {
+        if (!iface.checkLevel()) {
+          gatherMethods(this, iface, methods);
+        }
+      }
+      // mine
+      if (mAllSelfMethods != null) {
+        for (int i = 0; i < mAllSelfMethods.length; i++) {
+          MethodInfo m = mAllSelfMethods[i];
+          if (m.checkLevel()) {
+            methods.put(m.name() + m.signature(), m);
+          }
+        }
+      }
+      
+      // combine and return it
+      mSelfMethods = methods.values().toArray(new MethodInfo[methods.size()]);
+      Arrays.sort(mSelfMethods, MethodInfo.comparator);
+    }
+    return mSelfMethods;
+  }
+
+  public MethodInfo[] allSelfMethods() {
+    return mAllSelfMethods;
+  }
+
+  public void addMethod(MethodInfo method) {
+    mApiCheckMethods.put(method.getHashableName(), method);
+    
+    if (mAllSelfMethods == null) {
+      mAllSelfMethods = new MethodInfo[] { method };
+      return;
+    }
+    
+    MethodInfo[] methods = new MethodInfo[mAllSelfMethods.length + 1];
+    int i = 0;
+    for (MethodInfo m : mAllSelfMethods) {
+      methods[i++] = m;
+    }
+    methods[i] = method;
+    mAllSelfMethods = methods;
+  }
+  
+  public void setContainingPackage(PackageInfo pkg) {
+    mContainingPackage = pkg;
+  }
+
+  public AttributeInfo[] selfAttributes() {
+    if (mSelfAttributes == null) {
+      TreeMap<FieldInfo, AttributeInfo> attrs = new TreeMap<FieldInfo, AttributeInfo>();
+
+      // the ones in the class comment won't have any methods
+      for (AttrTagInfo tag : comment().attrTags()) {
+        FieldInfo field = tag.reference();
+        if (field != null) {
+          AttributeInfo attr = attrs.get(field);
+          if (attr == null) {
+            attr = new AttributeInfo(this, field);
+            attrs.put(field, attr);
+          }
+          tag.setAttribute(attr);
+        }
+      }
+
+      // in the methods
+      for (MethodInfo m : selfMethods()) {
+        for (AttrTagInfo tag : m.comment().attrTags()) {
+          FieldInfo field = tag.reference();
+          if (field != null) {
+            AttributeInfo attr = attrs.get(field);
+            if (attr == null) {
+              attr = new AttributeInfo(this, field);
+              attrs.put(field, attr);
+            }
+            tag.setAttribute(attr);
+            attr.methods.add(m);
+          }
+        }
+      }
+
+      // constructors too
+      for (MethodInfo m : constructors()) {
+        for (AttrTagInfo tag : m.comment().attrTags()) {
+          FieldInfo field = tag.reference();
+          if (field != null) {
+            AttributeInfo attr = attrs.get(field);
+            if (attr == null) {
+              attr = new AttributeInfo(this, field);
+              attrs.put(field, attr);
+            }
+            tag.setAttribute(attr);
+            attr.methods.add(m);
+          }
+        }
+      }
+
+      mSelfAttributes = attrs.values().toArray(new AttributeInfo[attrs.size()]);
+      Arrays.sort(mSelfAttributes, AttributeInfo.comparator);
+    }
+    return mSelfAttributes;
+  }
+
+  public FieldInfo[] enumConstants() {
+    return mEnumConstants;
+  }
+
+  public ClassInfo superclass() {
+    if (!mSuperclassInit) {
+      if (this.checkLevel()) {
+        // rearrange our little inheritance hierarchy, because we need to hide classes that
+        // don't pass checkLevel
+        ClassInfo superclass = mRealSuperclass;
+        while (superclass != null && !superclass.checkLevel()) {
+          superclass = superclass.mRealSuperclass;
+        }
+        mSuperclass = superclass;
+      } else {
+        mSuperclass = mRealSuperclass;
+      }
+    }
+    return mSuperclass;
+  }
+
+  public ClassInfo realSuperclass() {
+    return mRealSuperclass;
+  }
+
+  /**
+   * always the real superclass, not the collapsed one we get through superclass(), also has the
+   * type parameter info if it's generic.
+   */
+  public TypeInfo superclassType() {
+    return mRealSuperclassType;
+  }
+
+  public TypeInfo asTypeInfo() {
+    return mTypeInfo;
+  }
+
+  TypeInfo[] interfaceTypes() {
+    ClassInfo[] infos = interfaces();
+    int len = infos.length;
+    TypeInfo[] types = new TypeInfo[len];
+    for (int i = 0; i < len; i++) {
+      types[i] = infos[i].asTypeInfo();
+    }
+    return types;
+  }
+
+  public String htmlPage() {
+    String s = containingPackage().name();
+    s = s.replace('.', '/');
+    s += '/';
+    s += name();
+    s += ".html";
+    s = Doclava.javadocDir + s;
+    return s;
+  }
+
+  /** Even indirectly */
+  public boolean isDerivedFrom(ClassInfo cl) {
+    ClassInfo dad = this.superclass();
+    if (dad != null) {
+      if (dad.equals(cl)) {
+        return true;
+      } else {
+        if (dad.isDerivedFrom(cl)) {
+          return true;
+        }
+      }
+    }
+    for (ClassInfo iface : interfaces()) {
+      if (iface.equals(cl)) {
+        return true;
+      } else {
+        if (iface.isDerivedFrom(cl)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  public void makeKeywordEntries(List<KeywordEntry> keywords) {
+    if (!checkLevel()) {
+      return;
+    }
+
+    String htmlPage = htmlPage();
+    String qualifiedName = qualifiedName();
+
+    keywords.add(new KeywordEntry(name(), htmlPage, "class in " + containingPackage().name()));
+
+    FieldInfo[] fields = selfFields();
+    FieldInfo[] enumConstants = enumConstants();
+    MethodInfo[] ctors = constructors();
+    MethodInfo[] methods = selfMethods();
+
+    // enum constants
+    for (FieldInfo field : enumConstants()) {
+      if (field.checkLevel()) {
+        keywords.add(new KeywordEntry(field.name(), htmlPage + "#" + field.anchor(),
+            "enum constant in " + qualifiedName));
+      }
+    }
+
+    // constants
+    for (FieldInfo field : fields) {
+      if (field.isConstant() && field.checkLevel()) {
+        keywords.add(new KeywordEntry(field.name(), htmlPage + "#" + field.anchor(), "constant in "
+            + qualifiedName));
+      }
+    }
+
+    // fields
+    for (FieldInfo field : fields) {
+      if (!field.isConstant() && field.checkLevel()) {
+        keywords.add(new KeywordEntry(field.name(), htmlPage + "#" + field.anchor(), "field in "
+            + qualifiedName));
+      }
+    }
+
+    // public constructors
+    for (MethodInfo m : ctors) {
+      if (m.isPublic() && m.checkLevel()) {
+        keywords.add(new KeywordEntry(m.prettySignature(), htmlPage + "#" + m.anchor(),
+            "constructor in " + qualifiedName));
+      }
+    }
+
+    // protected constructors
+    if (Doclava.checkLevel(Doclava.SHOW_PROTECTED)) {
+      for (MethodInfo m : ctors) {
+        if (m.isProtected() && m.checkLevel()) {
+          keywords.add(new KeywordEntry(m.prettySignature(),
+              htmlPage + "#" + m.anchor(), "constructor in " + qualifiedName));
+        }
+      }
+    }
+
+    // package private constructors
+    if (Doclava.checkLevel(Doclava.SHOW_PACKAGE)) {
+      for (MethodInfo m : ctors) {
+        if (m.isPackagePrivate() && m.checkLevel()) {
+          keywords.add(new KeywordEntry(m.prettySignature(),
+              htmlPage + "#" + m.anchor(), "constructor in " + qualifiedName));
+        }
+      }
+    }
+
+    // private constructors
+    if (Doclava.checkLevel(Doclava.SHOW_PRIVATE)) {
+      for (MethodInfo m : ctors) {
+        if (m.isPrivate() && m.checkLevel()) {
+          keywords.add(new KeywordEntry(m.name() + m.prettySignature(),
+              htmlPage + "#" + m.anchor(), "constructor in " + qualifiedName));
+        }
+      }
+    }
+
+    // public methods
+    for (MethodInfo m : methods) {
+      if (m.isPublic() && m.checkLevel()) {
+        keywords.add(new KeywordEntry(m.name() + m.prettySignature(), htmlPage + "#" + m.anchor(),
+            "method in " + qualifiedName));
+      }
+    }
+
+    // protected methods
+    if (Doclava.checkLevel(Doclava.SHOW_PROTECTED)) {
+      for (MethodInfo m : methods) {
+        if (m.isProtected() && m.checkLevel()) {
+          keywords.add(new KeywordEntry(m.name() + m.prettySignature(),
+              htmlPage + "#" + m.anchor(), "method in " + qualifiedName));
+        }
+      }
+    }
+
+    // package private methods
+    if (Doclava.checkLevel(Doclava.SHOW_PACKAGE)) {
+      for (MethodInfo m : methods) {
+        if (m.isPackagePrivate() && m.checkLevel()) {
+          keywords.add(new KeywordEntry(m.name() + m.prettySignature(),
+              htmlPage + "#" + m.anchor(), "method in " + qualifiedName));
+        }
+      }
+    }
+
+    // private methods
+    if (Doclava.checkLevel(Doclava.SHOW_PRIVATE)) {
+      for (MethodInfo m : methods) {
+        if (m.isPrivate() && m.checkLevel()) {
+          keywords.add(new KeywordEntry(m.name() + m.prettySignature(),
+              htmlPage + "#" + m.anchor(), "method in " + qualifiedName));
+        }
+      }
+    }
+  }
+
+  public void makeLink(Data data, String base) {
+    data.setValue(base + ".label", this.name());
+    if (!this.isPrimitive() && this.isIncluded() && this.checkLevel()) {
+      data.setValue(base + ".link", this.htmlPage());
+    }
+  }
+
+  public static void makeLinkListHDF(Data data, String base, ClassInfo[] classes) {
+    final int N = classes.length;
+    for (int i = 0; i < N; i++) {
+      ClassInfo cl = classes[i];
+      if (cl.checkLevel()) {
+        cl.asTypeInfo().makeHDF(data, base + "." + i);
+      }
+    }
+  }
+
+  /**
+   * Used in lists of this class (packages, nested classes, known subclasses)
+   */
+  public void makeShortDescrHDF(Data data, String base) {
+    mTypeInfo.makeHDF(data, base + ".type");
+    data.setValue(base + ".kind", this.kind());
+    TagInfo.makeHDF(data, base + ".shortDescr", this.firstSentenceTags());
+    TagInfo.makeHDF(data, base + ".deprecated", deprecatedTags());
+    data.setValue(base + ".since", getSince());
+    setFederatedReferences(data, base);
+  }
+
+  /**
+   * Turns into the main class page
+   */
+  public void makeHDF(Data data) {
+    int i, j, n;
+    String name = name();
+    String qualified = qualifiedName();
+    AttributeInfo[] selfAttributes = selfAttributes();
+    MethodInfo[] methods = selfMethods();
+    FieldInfo[] fields = selfFields();
+    FieldInfo[] enumConstants = enumConstants();
+    MethodInfo[] ctors = constructors();
+    ClassInfo[] inners = innerClasses();
+
+    // class name
+    mTypeInfo.makeHDF(data, "class.type");
+    mTypeInfo.makeQualifiedHDF(data, "class.qualifiedType");
+    data.setValue("class.name", name);
+    data.setValue("class.qualified", qualified);
+    String scope = "";
+    if (isProtected()) {
+      data.setValue("class.scope", "protected");
+    } else if (isPublic()) {
+      data.setValue("class.scope", "public");
+    }
+    if (isStatic()) {
+      data.setValue("class.static", "static");
+    }
+    if (isFinal()) {
+      data.setValue("class.final", "final");
+    }
+    if (isAbstract() && !isInterface()) {
+      data.setValue("class.abstract", "abstract");
+    }
+
+    // class info
+    String kind = kind();
+    if (kind != null) {
+      data.setValue("class.kind", kind);
+    }
+    data.setValue("class.since", getSince());
+    setFederatedReferences(data, "class");
+
+    // the containing package -- note that this can be passed to type_link,
+    // but it also contains the list of all of the packages
+    containingPackage().makeClassLinkListHDF(data, "class.package");
+
+    // inheritance hierarchy
+    Vector<ClassInfo> superClasses = new Vector<ClassInfo>();
+    superClasses.add(this);
+    ClassInfo supr = superclass();
+    while (supr != null) {
+      superClasses.add(supr);
+      supr = supr.superclass();
+    }
+    n = superClasses.size();
+    for (i = 0; i < n; i++) {
+      supr = superClasses.elementAt(n - i - 1);
+
+      supr.asTypeInfo().makeQualifiedHDF(data, "class.inheritance." + i + ".class");
+      supr.asTypeInfo().makeHDF(data, "class.inheritance." + i + ".short_class");
+      j = 0;
+      for (TypeInfo t : supr.interfaceTypes()) {
+        t.makeHDF(data, "class.inheritance." + i + ".interfaces." + j);
+        j++;
+      }
+    }
+
+    // class description
+    TagInfo.makeHDF(data, "class.descr", inlineTags());
+    TagInfo.makeHDF(data, "class.seeAlso", comment().seeTags());
+    TagInfo.makeHDF(data, "class.deprecated", deprecatedTags());
+
+    // known subclasses
+    TreeMap<String, ClassInfo> direct = new TreeMap<String, ClassInfo>();
+    TreeMap<String, ClassInfo> indirect = new TreeMap<String, ClassInfo>();
+    ClassInfo[] all = Converter.rootClasses();
+    for (ClassInfo cl : all) {
+      if (cl.superclass() != null && cl.superclass().equals(this)) {
+        direct.put(cl.name(), cl);
+      } else if (cl.isDerivedFrom(this)) {
+        indirect.put(cl.name(), cl);
+      }
+    }
+    // direct
+    i = 0;
+    for (ClassInfo cl : direct.values()) {
+      if (cl.checkLevel()) {
+        cl.makeShortDescrHDF(data, "class.subclasses.direct." + i);
+      }
+      i++;
+    }
+    // indirect
+    i = 0;
+    for (ClassInfo cl : indirect.values()) {
+      if (cl.checkLevel()) {
+        cl.makeShortDescrHDF(data, "class.subclasses.indirect." + i);
+      }
+      i++;
+    }
+
+    // hide special cases
+    if ("java.lang.Object".equals(qualified) || "java.io.Serializable".equals(qualified)) {
+      data.setValue("class.subclasses.hidden", "1");
+    } else {
+      data.setValue("class.subclasses.hidden", "0");
+    }
+
+    // nested classes
+    i = 0;
+    for (ClassInfo inner : inners) {
+      if (inner.checkLevel()) {
+        inner.makeShortDescrHDF(data, "class.inners." + i);
+      }
+      i++;
+    }
+
+    // enum constants
+    i = 0;
+    for (FieldInfo field : enumConstants) {
+      if (field.isConstant()) {
+        field.makeHDF(data, "class.enumConstants." + i);
+        i++;
+      }
+    }
+
+    // constants
+    i = 0;
+    for (FieldInfo field : fields) {
+      if (field.isConstant()) {
+        field.makeHDF(data, "class.constants." + i);
+        i++;
+      }
+    }
+
+    // fields
+    i = 0;
+    for (FieldInfo field : fields) {
+      if (!field.isConstant()) {
+        field.makeHDF(data, "class.fields." + i);
+        i++;
+      }
+    }
+
+    // public constructors
+    i = 0;
+    for (MethodInfo ctor : ctors) {
+      if (ctor.isPublic()) {
+        ctor.makeHDF(data, "class.ctors.public." + i);
+        i++;
+      }
+    }
+
+    // protected constructors
+    if (Doclava.checkLevel(Doclava.SHOW_PROTECTED)) {
+      i = 0;
+      for (MethodInfo ctor : ctors) {
+        if (ctor.isProtected()) {
+          ctor.makeHDF(data, "class.ctors.protected." + i);
+          i++;
+        }
+      }
+    }
+
+    // package private constructors
+    if (Doclava.checkLevel(Doclava.SHOW_PACKAGE)) {
+      i = 0;
+      for (MethodInfo ctor : ctors) {
+        if (ctor.isPackagePrivate()) {
+          ctor.makeHDF(data, "class.ctors.package." + i);
+          i++;
+        }
+      }
+    }
+
+    // private constructors
+    if (Doclava.checkLevel(Doclava.SHOW_PRIVATE)) {
+      i = 0;
+      for (MethodInfo ctor : ctors) {
+        if (ctor.isPrivate()) {
+          ctor.makeHDF(data, "class.ctors.private." + i);
+          i++;
+        }
+      }
+    }
+
+    // public methods
+    i = 0;
+    for (MethodInfo method : methods) {
+      if (method.isPublic()) {
+        method.makeHDF(data, "class.methods.public." + i);
+        i++;
+      }
+    }
+
+    // protected methods
+    if (Doclava.checkLevel(Doclava.SHOW_PROTECTED)) {
+      i = 0;
+      for (MethodInfo method : methods) {
+        if (method.isProtected()) {
+          method.makeHDF(data, "class.methods.protected." + i);
+          i++;
+        }
+      }
+    }
+
+    // package private methods
+    if (Doclava.checkLevel(Doclava.SHOW_PACKAGE)) {
+      i = 0;
+      for (MethodInfo method : methods) {
+        if (method.isPackagePrivate()) {
+          method.makeHDF(data, "class.methods.package." + i);
+          i++;
+        }
+      }
+    }
+
+    // private methods
+    if (Doclava.checkLevel(Doclava.SHOW_PRIVATE)) {
+      i = 0;
+      for (MethodInfo method : methods) {
+        if (method.isPrivate()) {
+          method.makeHDF(data, "class.methods.private." + i);
+          i++;
+        }
+      }
+    }
+
+    // xml attributes
+    i = 0;
+    for (AttributeInfo attr : selfAttributes) {
+      if (attr.checkLevel()) {
+        attr.makeHDF(data, "class.attrs." + i);
+        i++;
+      }
+    }
+
+    // inherited methods
+    Set<ClassInfo> interfaces = new TreeSet<ClassInfo>();
+    addInterfaces(interfaces(), interfaces);
+    ClassInfo cl = superclass();
+    i = 0;
+    while (cl != null) {
+      addInterfaces(cl.interfaces(), interfaces);
+      makeInheritedHDF(data, i, cl);
+      cl = cl.superclass();
+      i++;
+    }
+    for (ClassInfo iface : interfaces) {
+      makeInheritedHDF(data, i, iface);
+      i++;
+    }
+  }
+
+  private static void addInterfaces(ClassInfo[] ifaces, Set<ClassInfo> out) {
+    for (ClassInfo cl : ifaces) {
+      out.add(cl);
+      addInterfaces(cl.interfaces(), out);
+    }
+  }
+
+  private static void makeInheritedHDF(Data data, int index, ClassInfo cl) {
+    int i;
+
+    String base = "class.inherited." + index;
+    data.setValue(base + ".qualified", cl.qualifiedName());
+    if (cl.checkLevel()) {
+      data.setValue(base + ".link", cl.htmlPage());
+    }
+    String kind = cl.kind();
+    if (kind != null) {
+      data.setValue(base + ".kind", kind);
+    }
+
+    if (cl.mIsIncluded) {
+      data.setValue(base + ".included", "true");
+    } else {
+      Doclava.federationTagger.tagAll(new ClassInfo[] {cl});
+      if (!cl.getFederatedReferences().isEmpty()) {
+        FederatedSite site = cl.getFederatedReferences().iterator().next();
+        data.setValue(base + ".link", site.linkFor(cl.htmlPage()));
+        data.setValue(base + ".federated", site.name());
+      }
+    }
+
+    // xml attributes
+    i = 0;
+    for (AttributeInfo attr : cl.selfAttributes()) {
+      attr.makeHDF(data, base + ".attrs." + i);
+      i++;
+    }
+
+    // methods
+    i = 0;
+    for (MethodInfo method : cl.selfMethods()) {
+      method.makeHDF(data, base + ".methods." + i);
+      i++;
+    }
+
+    // fields
+    i = 0;
+    for (FieldInfo field : cl.selfFields()) {
+      if (!field.isConstant()) {
+        field.makeHDF(data, base + ".fields." + i);
+        i++;
+      }
+    }
+
+    // constants
+    i = 0;
+    for (FieldInfo field : cl.selfFields()) {
+      if (field.isConstant()) {
+        field.makeHDF(data, base + ".constants." + i);
+        i++;
+      }
+    }
+  }
+
+  @Override
+  public boolean isHidden() {
+    int val = mHidden;
+    if (val >= 0) {
+      return val != 0;
+    } else {
+      boolean v = isHiddenImpl();
+      mHidden = v ? 1 : 0;
+      return v;
+    }
+  }
+
+  public boolean isHiddenImpl() {
+    ClassInfo cl = this;
+    while (cl != null) {
+      PackageInfo pkg = cl.containingPackage();
+      if (pkg != null && pkg.isHidden()) {
+        return true;
+      }
+      if (cl.comment().isHidden()) {
+        return true;
+      }
+      cl = cl.containingClass();
+    }
+    return false;
+  }
+
+  private MethodInfo matchMethod(MethodInfo[] methods, String name, String[] params,
+      String[] dimensions, boolean varargs) {
+    int len = methods.length;
+    for (int i = 0; i < len; i++) {
+      MethodInfo method = methods[i];
+      if (method.name().equals(name)) {
+        if (params == null) {
+          return method;
+        } else {
+          if (method.matchesParams(params, dimensions, varargs)) {
+            return method;
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  public MethodInfo findMethod(String name, String[] params, String[] dimensions, boolean varargs) {
+    // first look on our class, and our superclasses
+
+    // for methods
+    MethodInfo rv;
+    rv = matchMethod(methods(), name, params, dimensions, varargs);
+
+    if (rv != null) {
+      return rv;
+    }
+
+    // for constructors
+    rv = matchMethod(constructors(), name, params, dimensions, varargs);
+    if (rv != null) {
+      return rv;
+    }
+
+    // then recursively look at our containing class
+    ClassInfo containing = containingClass();
+    if (containing != null) {
+      return containing.findMethod(name, params, dimensions, varargs);
+    }
+
+    return null;
+  }
+  
+  public boolean supportsMethod(MethodInfo method) {
+    for (MethodInfo m : methods()) {
+      if (m.getHashableName().equals(method.getHashableName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private ClassInfo searchInnerClasses(String[] nameParts, int index) {
+    String part = nameParts[index];
+
+    ClassInfo[] inners = mInnerClasses;
+    for (ClassInfo in : inners) {
+      String[] innerParts = in.nameParts();
+      if (part.equals(innerParts[innerParts.length - 1])) {
+        if (index == nameParts.length - 1) {
+          return in;
+        } else {
+          return in.searchInnerClasses(nameParts, index + 1);
+        }
+      }
+    }
+    return null;
+  }
+
+  public ClassInfo extendedFindClass(String className) {
+    // ClassDoc.findClass has this bug that we're working around here:
+    // If you have a class PackageManager with an inner class PackageInfo
+    // and you call it with "PackageInfo" it doesn't find it.
+    return searchInnerClasses(className.split("\\."), 0);
+  }
+
+  public ClassInfo findClass(String className) {
+    return Converter.obtainClass(mClass.findClass(className));
+  }
+
+  public ClassInfo findInnerClass(String className) {
+    // ClassDoc.findClass won't find inner classes. To deal with that,
+    // we try what they gave us first, but if that didn't work, then
+    // we see if there are any periods in className, and start searching
+    // from there.
+    String[] nodes = className.split("\\.");
+    ClassDoc cl = mClass;
+    for (String n : nodes) {
+      cl = cl.findClass(n);
+      if (cl == null) {
+        return null;
+      }
+    }
+    return Converter.obtainClass(cl);
+  }
+
+  public FieldInfo findField(String name) {
+    // first look on our class, and our superclasses
+    for (FieldInfo f : fields()) {
+      if (f.name().equals(name)) {
+        return f;
+      }
+    }
+
+    // then look at our enum constants (these are really fields, maybe
+    // they should be mixed into fields(). not sure)
+    for (FieldInfo f : enumConstants()) {
+      if (f.name().equals(name)) {
+        return f;
+      }
+    }
+
+    // then recursively look at our containing class
+    ClassInfo containing = containingClass();
+    if (containing != null) {
+      return containing.findField(name);
+    }
+
+    return null;
+  }
+
+  public static ClassInfo[] sortByName(ClassInfo[] classes) {
+    int i;
+    Sorter[] sorted = new Sorter[classes.length];
+    for (i = 0; i < sorted.length; i++) {
+      ClassInfo cl = classes[i];
+      sorted[i] = new Sorter(cl.name(), cl);
+    }
+
+    Arrays.sort(sorted);
+
+    ClassInfo[] rv = new ClassInfo[classes.length];
+    for (i = 0; i < rv.length; i++) {
+      rv[i] = (ClassInfo) sorted[i].data;
+    }
+
+    return rv;
+  }
+
+  public boolean equals(ClassInfo that) {
+    if (that != null) {
+      return this.qualifiedName().equals(that.qualifiedName());
+    } else {
+      return false;
+    }
+  }
+
+  public void setNonWrittenConstructors(MethodInfo[] nonWritten) {
+    mNonWrittenConstructors = nonWritten;
+  }
+
+  public MethodInfo[] getNonWrittenConstructors() {
+    return mNonWrittenConstructors;
+  }
+
+  public String kind() {
+    if (isOrdinaryClass()) {
+      return "class";
+    } else if (isInterface()) {
+      return "interface";
+    } else if (isEnum()) {
+      return "enum";
+    } else if (isError()) {
+      return "class";
+    } else if (isException()) {
+      return "class";
+    } else if (isAnnotation()) {
+      return "@interface";
+    }
+    return null;
+  }
+  
+  public String scope() {
+    if (isPublic()) {
+      return "public";
+    } else if (isProtected()) {
+      return "protected";
+    } else if (isPackagePrivate()) {
+      return "";
+    } else if (isPrivate()) {
+      return "private";
+    } else {
+      throw new RuntimeException("invalid scope for object " + this);
+    }
+  }
+
+  public void setHiddenMethods(MethodInfo[] mInfo) {
+    mHiddenMethods = mInfo;
+  }
+
+  public MethodInfo[] getHiddenMethods() {
+    return mHiddenMethods;
+  }
+
+  @Override
+  public String toString() {
+    return this.qualifiedName();
+  }
+
+  public void setReasonIncluded(String reason) {
+    mReasonIncluded = reason;
+  }
+
+  public String getReasonIncluded() {
+    return mReasonIncluded;
+  }
+
+  private ClassDoc mClass;
+
+  // ctor
+  private boolean mIsPublic;
+  private boolean mIsProtected;
+  private boolean mIsPackagePrivate;
+  private boolean mIsPrivate;
+  private boolean mIsStatic;
+  private boolean mIsInterface;
+  private boolean mIsAbstract;
+  private boolean mIsOrdinaryClass;
+  private boolean mIsException;
+  private boolean mIsError;
+  private boolean mIsEnum;
+  private boolean mIsAnnotation;
+  private boolean mIsFinal;
+  private boolean mIsIncluded;
+  private String mName;
+  private String mQualifiedName;
+  private String mQualifiedTypeName;
+  private boolean mIsPrimitive;
+  private TypeInfo mTypeInfo;
+  private String[] mNameParts;
+
+  // init
+  private List<ClassInfo> mRealInterfaces = new ArrayList<ClassInfo>();
+  private ClassInfo[] mInterfaces;
+  private TypeInfo[] mRealInterfaceTypes;
+  private ClassInfo[] mInnerClasses;
+  private MethodInfo[] mAllConstructors;
+  private MethodInfo[] mAllSelfMethods;
+  private MethodInfo[] mAnnotationElements; // if this class is an annotation
+  private FieldInfo[] mAllSelfFields;
+  private FieldInfo[] mEnumConstants;
+  private PackageInfo mContainingPackage;
+  private ClassInfo mContainingClass;
+  private ClassInfo mRealSuperclass;
+  private TypeInfo mRealSuperclassType;
+  private ClassInfo mSuperclass;
+  private AnnotationInstanceInfo[] mAnnotations;
+  private boolean mSuperclassInit;
+  private boolean mDeprecatedKnown;
+
+  // lazy
+  private MethodInfo[] mConstructors;
+  private ClassInfo[] mRealInnerClasses;
+  private MethodInfo[] mSelfMethods;
+  private FieldInfo[] mSelfFields;
+  private AttributeInfo[] mSelfAttributes;
+  private MethodInfo[] mMethods;
+  private FieldInfo[] mFields;
+  private TypeInfo[] mTypeParameters;
+  private MethodInfo[] mHiddenMethods;
+  private int mHidden = -1;
+  private int mCheckLevel = -1;
+  private String mReasonIncluded;
+  private MethodInfo[] mNonWrittenConstructors;
+  private boolean mIsDeprecated;
+  
+  // TODO: Temporary members from apicheck migration.
+  private HashMap<String, MethodInfo> mApiCheckMethods = new HashMap<String, MethodInfo>();
+  private HashMap<String, FieldInfo> mApiCheckFields = new HashMap<String, FieldInfo>();
+  private HashMap<String, ConstructorInfo> mApiCheckConstructors
+      = new HashMap<String, ConstructorInfo>();
+  
+  /**
+   * Returns true if {@code cl} implements the interface {@code iface} either by either being that
+   * interface, implementing that interface or extending a type that implements the interface.
+   */
+  private boolean implementsInterface(ClassInfo cl, String iface) {
+    if (cl.qualifiedName().equals(iface)) {
+      return true;
+    }
+    for (ClassInfo clImplements : cl.interfaces()) {
+      if (implementsInterface(clImplements, iface)) {
+        return true;
+      }
+    }
+    if (cl.mSuperclass != null && implementsInterface(cl.mSuperclass, iface)) {
+      return true;
+    }
+    return false;
+  }
+
+
+  public void addInterface(ClassInfo iface) {
+    mRealInterfaces.add(iface);
+  }
+
+  public void addConstructor(ConstructorInfo cInfo) {
+    mApiCheckConstructors.put(cInfo.getHashableName(), cInfo);
+
+  }
+
+  public void addField(FieldInfo fInfo) {
+    mApiCheckFields.put(fInfo.name(), fInfo);
+
+  }
+
+  public void setSuperClass(ClassInfo superclass) {
+    mSuperclass = superclass;
+  }
+
+  public Map<String, ConstructorInfo> allConstructorsMap() {
+    return mApiCheckConstructors;
+  }
+
+  public Map<String, FieldInfo> allFields() {
+    return mApiCheckFields;
+  }
+
+  /**
+   * Returns all methods defined directly in this class. For a list of all
+   * methods supported by this class, see {@link #methods()}.
+   */
+  
+  public Map<String, MethodInfo> allMethods() {
+    return mApiCheckMethods;
+  }
+
+  /**
+   * Returns the class hierarchy for this class, starting with this class.
+   */
+  public Iterable<ClassInfo> hierarchy() {
+    List<ClassInfo> result = new ArrayList<ClassInfo>(4);
+    for (ClassInfo c = this; c != null; c = c.mSuperclass) {
+      result.add(c);
+    }
+    return result;
+  }
+  
+  public String superclassName() {
+    if (mSuperclass == null) {
+      if (mQualifiedName.equals("java.lang.Object")) {
+        return null;
+      }
+      throw new UnsupportedOperationException("Superclass not set for " + qualifiedName());
+    }
+    return mSuperclass.mQualifiedName;
+  }
+  
+  public void setAnnotations(AnnotationInstanceInfo[] annotations) {
+    mAnnotations = annotations;
+  }
+  
+  public boolean isConsistent(ClassInfo cl) {
+    boolean consistent = true;
+
+    if (isInterface() != cl.isInterface()) {
+      Errors.error(Errors.CHANGED_CLASS, cl.position(), "Class " + cl.qualifiedName()
+          + " changed class/interface declaration");
+      consistent = false;
+    }
+    for (ClassInfo iface : mRealInterfaces) {
+      if (!implementsInterface(cl, iface.mQualifiedName)) {
+        Errors.error(Errors.REMOVED_INTERFACE, cl.position(), "Class " + qualifiedName()
+            + " no longer implements " + iface);
+      }
+    }
+    for (ClassInfo iface : cl.mRealInterfaces) {
+      if (!implementsInterface(this, iface.mQualifiedName)) {
+        Errors.error(Errors.ADDED_INTERFACE, cl.position(), "Added interface " + iface
+            + " to class " + qualifiedName());
+        consistent = false;
+      }
+    }
+
+    for (MethodInfo mInfo : mApiCheckMethods.values()) {
+      if (cl.mApiCheckMethods.containsKey(mInfo.getHashableName())) {
+        if (!mInfo.isConsistent(cl.mApiCheckMethods.get(mInfo.getHashableName()))) {
+          consistent = false;
+        }
+      } else {
+        /*
+         * This class formerly provided this method directly, and now does not. Check our ancestry
+         * to see if there's an inherited version that still fulfills the API requirement.
+         */
+        MethodInfo mi = ClassInfo.overriddenMethod(mInfo, cl);
+        if (mi == null) {
+          mi = ClassInfo.interfaceMethod(mInfo, cl);
+        }
+        if (mi == null) {
+          Errors.error(Errors.REMOVED_METHOD, mInfo.position(), "Removed public method "
+              + mInfo.qualifiedName());
+          consistent = false;
+        }
+      }
+    }
+    for (MethodInfo mInfo : cl.mApiCheckMethods.values()) {
+      if (!mApiCheckMethods.containsKey(mInfo.getHashableName())) {
+        /*
+         * Similarly to the above, do not fail if this "new" method is really an override of an
+         * existing superclass method.
+         */
+        MethodInfo mi = ClassInfo.overriddenMethod(mInfo, this);
+        if (mi == null) {
+          Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public method "
+              + mInfo.qualifiedName());
+          consistent = false;
+        }
+      }
+    }
+
+    for (ConstructorInfo mInfo : mApiCheckConstructors.values()) {
+      if (cl.mApiCheckConstructors.containsKey(mInfo.getHashableName())) {
+        if (!mInfo.isConsistent(cl.mApiCheckConstructors.get(mInfo.getHashableName()))) {
+          consistent = false;
+        }
+      } else {
+        Errors.error(Errors.REMOVED_METHOD, mInfo.position(), "Removed public constructor "
+            + mInfo.prettySignature());
+        consistent = false;
+      }
+    }
+    for (ConstructorInfo mInfo : cl.mApiCheckConstructors.values()) {
+      if (!mApiCheckConstructors.containsKey(mInfo.getHashableName())) {
+        Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public constructor "
+            + mInfo.prettySignature());
+        consistent = false;
+      }
+    }
+
+    for (FieldInfo mInfo : mApiCheckFields.values()) {
+      if (cl.mApiCheckFields.containsKey(mInfo.name())) {
+        if (!mInfo.isConsistent(cl.mApiCheckFields.get(mInfo.name()))) {
+          consistent = false;
+        }
+      } else {
+        Errors.error(Errors.REMOVED_FIELD, mInfo.position(), "Removed field "
+            + mInfo.qualifiedName());
+        consistent = false;
+      }
+    }
+    for (FieldInfo mInfo : cl.mApiCheckFields.values()) {
+      if (!mApiCheckFields.containsKey(mInfo.name())) {
+        Errors.error(Errors.ADDED_FIELD, mInfo.position(), "Added public field "
+            + mInfo.qualifiedName());
+        consistent = false;
+      }
+    }
+
+    if (mIsAbstract != cl.mIsAbstract) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_ABSTRACT, cl.position(), "Class " + cl.qualifiedName()
+          + " changed abstract qualifier");
+    }
+
+    if (mIsFinal != cl.mIsFinal) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_FINAL, cl.position(), "Class " + cl.qualifiedName()
+          + " changed final qualifier");
+    }
+
+    if (mIsStatic != cl.mIsStatic) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_STATIC, cl.position(), "Class " + cl.qualifiedName()
+          + " changed static qualifier");
+    }
+
+    if (!scope().equals(cl.scope())) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_SCOPE, cl.position(), "Class " + cl.qualifiedName()
+          + " scope changed from " + scope() + " to " + cl.scope());
+    }
+
+    if (!isDeprecated() == cl.isDeprecated()) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_DEPRECATED, cl.position(), "Class " + cl.qualifiedName()
+          + " has changed deprecation state");
+    }
+
+    if (superclassName() != null) {
+      if (cl.superclassName() == null || !superclassName().equals(cl.superclassName())) {
+        consistent = false;
+        Errors.error(Errors.CHANGED_SUPERCLASS, cl.position(), "Class " + qualifiedName()
+            + " superclass changed from " + superclassName() + " to " + cl.superclassName());
+      }
+    } else if (cl.superclassName() != null) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_SUPERCLASS, cl.position(), "Class " + qualifiedName()
+          + " superclass changed from " + "null to " + cl.superclassName());
+    }
+
+    return consistent;
+  }
+  
+  // Find a superclass implementation of the given method.
+  public static MethodInfo overriddenMethod(MethodInfo candidate, ClassInfo newClassObj) {
+    if (newClassObj == null) {
+      return null;
+    }
+    for (MethodInfo mi : newClassObj.mApiCheckMethods.values()) {
+      if (mi.matches(candidate)) {
+        // found it
+        return mi;
+      }
+    }
+
+    // not found here. recursively search ancestors
+    return ClassInfo.overriddenMethod(candidate, newClassObj.mSuperclass);
+  }
+
+  // Find a superinterface declaration of the given method.
+  public static MethodInfo interfaceMethod(MethodInfo candidate, ClassInfo newClassObj) {
+    if (newClassObj == null) {
+      return null;
+    }
+    for (ClassInfo interfaceInfo : newClassObj.interfaces()) {
+      for (MethodInfo mi : interfaceInfo.mApiCheckMethods.values()) {
+        if (mi.matches(candidate)) {
+          return mi;
+        }
+      }
+    }
+    return ClassInfo.interfaceMethod(candidate, newClassObj.mSuperclass);
+  }
+  
+  public boolean hasConstructor(MethodInfo constructor) {
+    String name = constructor.getHashableName();
+    for (ConstructorInfo ctor : mApiCheckConstructors.values()) {
+      if (name.equals(ctor.getHashableName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  public void setTypeInfo(TypeInfo typeInfo) {
+    mTypeInfo = typeInfo;
+  }
+}
diff --git a/src/com/google/doclava/ClearPage.java b/src/com/google/doclava/ClearPage.java
new file mode 100644
index 0000000..4bb0784
--- /dev/null
+++ b/src/com/google/doclava/ClearPage.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.JSilver;
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ClearPage {
+  /*
+   * public ClearPage() { String templ = "templates/index.cs"; String filename = "docs/index.html";
+   * 
+   * data.setValue("A.B.C", "1"); data.setValue("A.B.D", "2"); }
+   */
+
+  private static ArrayList<String> mTemplateDirs = new ArrayList<String>();
+  private static boolean mTemplateDirSet = false;
+
+  private static ArrayList<String> mBundledTemplateDirs = new ArrayList<String>();
+
+  public static String outputDir = "docs";
+  public static String htmlDir = null;
+  public static String toroot = null;
+
+  public static void addTemplateDir(String dir) {
+    mTemplateDirSet = true;
+    mTemplateDirs.add(dir);
+  }
+
+  public static List<String> getTemplateDirs() {
+    return mTemplateDirs;
+  }
+
+  public static void addBundledTemplateDir(String dir) {
+    mTemplateDirSet = true;
+    mBundledTemplateDirs.add(dir);
+  }
+
+  public static List<String> getBundledTemplateDirs() {
+    return mBundledTemplateDirs;
+  }
+
+  private static int countSlashes(String s) {
+    final int N = s.length();
+    int slashcount = 0;
+    for (int i = 0; i < N; i++) {
+      if (s.charAt(i) == '/') {
+        slashcount++;
+      }
+    }
+    return slashcount;
+  }
+
+  public static void write(Data data, String templ, String filename, JSilver cs) {
+    write(data, templ, filename, false, cs);
+  }
+
+  public static void write(Data data, String templ, String filename) {
+    write(data, templ, filename, false, Doclava.jSilver);
+  }
+
+  public static void write(Data data, String templ, String filename, boolean fullPath) {
+    write(data, templ, filename, false, Doclava.jSilver);
+  }
+
+  public static void write(Data data, String templ, String filename, boolean fullPath, JSilver cs) {
+    if (htmlDir != null) {
+      data.setValue("hasindex", "true");
+    }
+
+    String toroot;
+    if (ClearPage.toroot != null) {
+      toroot = ClearPage.toroot;
+    } else {
+      int slashcount = countSlashes(filename);
+      if (slashcount > 0) {
+        toroot = "";
+        for (int i = 0; i < slashcount; i++) {
+          toroot += "../";
+        }
+      } else {
+        toroot = "./";
+      }
+    }
+    data.setValue("toroot", toroot);
+
+    data.setValue("filename", filename);
+
+    if (!fullPath) {
+      filename = outputDir + "/" + filename;
+    }
+
+    int i = 0;
+    if (htmlDir != null) {
+      data.setValue("hdf.loadpaths." + i, htmlDir);
+      i++;
+    }
+    if (mTemplateDirSet) {
+      for (String dir : mTemplateDirs) {
+        data.setValue("hdf.loadpaths." + i, dir);
+        i++;
+      }
+    } else {
+      data.setValue("hdf.loadpaths." + i, "templates");
+    }
+
+    File file = new File(outputFilename(filename));
+
+    ensureDirectory(file);
+
+    OutputStreamWriter stream = null;
+    try {
+      stream = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
+      String rendered = cs.render(templ, data);
+      stream.write(rendered, 0, rendered.length());
+    } catch (IOException e) {
+      System.out.println("error: " + e.getMessage() + "; when writing file: " + filename);
+    } finally {
+      if (stream != null) {
+        try {
+          stream.close();
+        } catch (IOException e) {}
+      }
+    }
+  }
+
+  // recursively create the directories to the output
+  public static void ensureDirectory(File f) {
+    File parent = f.getParentFile();
+    if (parent != null) {
+      parent.mkdirs();
+    }
+  }
+
+  public static void copyFile(File from, String toPath) {
+    File to = new File(outputDir + "/" + toPath);
+    FileInputStream in;
+    FileOutputStream out;
+    try {
+      if (!from.exists()) {
+        throw new IOException();
+      }
+      in = new FileInputStream(from);
+    } catch (IOException e) {
+      System.err.println(from.getAbsolutePath() + ": Error opening file");
+      return;
+    }
+    ensureDirectory(to);
+    try {
+      out = new FileOutputStream(to);
+    } catch (IOException e) {
+      System.err.println(from.getAbsolutePath() + ": Error opening file");
+      return;
+    }
+
+    long sizel = from.length();
+    final int maxsize = 64 * 1024;
+    int size = sizel > maxsize ? maxsize : (int) sizel;
+    byte[] buf = new byte[size];
+    while (true) {
+      try {
+        size = in.read(buf);
+      } catch (IOException e) {
+        System.err.println(from.getAbsolutePath() + ": error reading file");
+        break;
+      }
+      if (size > 0) {
+        try {
+          out.write(buf, 0, size);
+        } catch (IOException e) {
+          System.err.println(from.getAbsolutePath() + ": error writing file");
+        }
+      } else {
+        break;
+      }
+    }
+    try {
+      in.close();
+    } catch (IOException e) {}
+    try {
+      out.close();
+    } catch (IOException e) {}
+  }
+
+  /** Takes a string that ends w/ .html and changes the .html to htmlExtension */
+  public static String outputFilename(String htmlFile) {
+    if (!Doclava.htmlExtension.equals(".html") && htmlFile.endsWith(".html")) {
+      return htmlFile.substring(0, htmlFile.length() - 5) + Doclava.htmlExtension;
+    } else {
+      return htmlFile;
+    }
+  }
+
+}
diff --git a/src/com/google/doclava/CodeTagInfo.java b/src/com/google/doclava/CodeTagInfo.java
new file mode 100644
index 0000000..1a4a864
--- /dev/null
+++ b/src/com/google/doclava/CodeTagInfo.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class CodeTagInfo extends TagInfo {
+  private static String encode(String t) {
+    t = t.replace("&", "&amp;");
+    t = t.replace("<", "&lt;");
+    t = t.replace(">", "&gt;");
+    return t;
+  }
+
+  public CodeTagInfo(String text, SourcePositionInfo sp) {
+    // TODO: the correct behavior is to escape the text,
+    // but we'll have to update the Android sources before making the switch.
+    //super("@code", "@code", encode(text), sp);
+    super("@code", "@code", text, sp);
+  }
+}
diff --git a/src/com/google/doclava/Comment.java b/src/com/google/doclava/Comment.java
new file mode 100644
index 0000000..178c17a
--- /dev/null
+++ b/src/com/google/doclava/Comment.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.util.ArrayList;
+
+public class Comment {
+  static final Pattern LEADING_WHITESPACE = Pattern.compile("^[ \t\n\r]*(.*)$", Pattern.DOTALL);
+
+  static final Pattern TAG_BEGIN = Pattern.compile("[\r\n][\r\n \t]*@", Pattern.DOTALL);
+
+  static final Pattern TAG = Pattern.compile("(@[^ \t\r\n]+)[ \t\r\n]+(.*)", Pattern.DOTALL);
+
+  static final Pattern INLINE_TAG =
+      Pattern.compile("(.*?)\\{(@[^ \t\r\n\\}]+)[ \t\r\n]*(.*?)\\}", Pattern.DOTALL);
+
+  static final Pattern FIRST_SENTENCE =
+      Pattern.compile("((.*?)\\.)[ \t\r\n\\<](.*)", Pattern.DOTALL);
+
+  private static final String[] KNOWN_TAGS =
+      new String[] {"@author", "@since", "@version", "@deprecated", "@undeprecate", "@docRoot",
+          "@sdkCurrent", "@inheritDoc", "@more", "@samplecode", "@sample", "@include",
+          "@serial", "@com.intel.drl.spec_ref", "@ar.org.fitc.spec_ref",};
+
+  public Comment(String text, ContainerInfo base, SourcePositionInfo sp) {
+    mText = text;
+    mBase = base;
+    // sp now points to the end of the text, not the beginning!
+    mPosition = SourcePositionInfo.findBeginning(sp, text);
+  }
+
+  private void parseRegex(String text) {
+    Matcher m;
+
+    m = LEADING_WHITESPACE.matcher(text);
+    m.matches();
+    text = m.group(1);
+
+    m = TAG_BEGIN.matcher(text);
+
+    int start = 0;
+    int end = 0;
+    while (m.find()) {
+      end = m.start();
+
+      tag(text, start, end);
+
+      start = m.end() - 1; // -1 is the @
+    }
+    end = text.length();
+    tag(text, start, end);
+  }
+
+  private void tag(String text, int start, int end) {
+    SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, start);
+
+    if (start >= 0 && end > 0 && (end - start) > 0) {
+      text = text.substring(start, end);
+
+      Matcher m = TAG.matcher(text);
+      if (m.matches()) {
+        // out of line tag
+        tag(m.group(1), m.group(2), false, pos);
+      } else {
+        // look for inline tags
+        m = INLINE_TAG.matcher(text);
+        start = 0;
+        while (m.find()) {
+          String str = m.group(1);
+          String tagname = m.group(2);
+          String tagvalue = m.group(3);
+          tag(null, m.group(1), true, pos);
+          tag(tagname, tagvalue, true, pos);
+          start = m.end();
+        }
+        int len = text.length();
+        if (start != len) {
+          tag(null, text.substring(start), true, pos);
+        }
+      }
+    }
+  }
+
+  private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) {
+    /*
+     * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" +
+     * name + "] text=[" + text + "]");
+     */
+    if (name == null) {
+      mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos));
+    } else if (name.equals("@param")) {
+      mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos));
+    } else if (name.equals("@see")) {
+      mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos));
+    } else if (name.equals("@link") || name.equals("@linkplain")) {
+      mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos));
+    } else if (name.equals("@throws") || name.equals("@exception")) {
+      mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos));
+    } else if (name.equals("@return")) {
+      mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos));
+    } else if (name.equals("@deprecated")) {
+      if (text.length() == 0) {
+        Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment");
+        text = "No replacement.";
+      }
+      mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos));
+    } else if (name.equals("@literal")) {
+      mInlineTagsList.add(new LiteralTagInfo(text, pos));
+    } else if (name.equals("@code")) {
+      mInlineTagsList.add(new CodeTagInfo(text, pos));
+    } else if (name.equals("@hide") || name.equals("@pending") || name.equals("@doconly")) {
+      // nothing
+    } else if (name.equals("@attr")) {
+      AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos);
+      mAttrTagsList.add(tag);
+      Comment c = tag.description();
+      if (c != null) {
+        for (TagInfo t : c.tags()) {
+          mInlineTagsList.add(t);
+        }
+      }
+    } else if (name.equals("@undeprecate")) {
+      mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos));
+    } 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;
+        }
+      }
+      if (!known) {
+        Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos),
+            "Unknown tag: " + name);
+      }
+      TagInfo t = new TextTagInfo(name, name, text, pos);
+      if (isInline) {
+        mInlineTagsList.add(t);
+      } else {
+        mTagsList.add(t);
+      }
+    }
+  }
+
+  private void parseBriefTags() {
+    int N = mInlineTagsList.size();
+
+    // look for "@more" tag, which means that we might go past the first sentence.
+    int more = -1;
+    for (int i = 0; i < N; i++) {
+      if (mInlineTagsList.get(i).name().equals("@more")) {
+        more = i;
+      }
+    }
+    if (more >= 0) {
+      for (int i = 0; i < more; i++) {
+        mBriefTagsList.add(mInlineTagsList.get(i));
+      }
+    } else {
+      for (int i = 0; i < N; i++) {
+        TagInfo t = mInlineTagsList.get(i);
+        if (t.name().equals("Text")) {
+          Matcher m = FIRST_SENTENCE.matcher(t.text());
+          if (m.matches()) {
+            String text = m.group(1);
+            TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position());
+            mBriefTagsList.add(firstSentenceTag);
+            break;
+          }
+        }
+        mBriefTagsList.add(t);
+
+      }
+    }
+  }
+
+  public TagInfo[] tags() {
+    init();
+    return mInlineTags;
+  }
+
+  public TagInfo[] tags(String name) {
+    init();
+    ArrayList<TagInfo> results = new ArrayList<TagInfo>();
+    int N = mInlineTagsList.size();
+    for (int i = 0; i < N; i++) {
+      TagInfo t = mInlineTagsList.get(i);
+      if (t.name().equals(name)) {
+        results.add(t);
+      }
+    }
+    return results.toArray(new TagInfo[results.size()]);
+  }
+
+  public ParamTagInfo[] paramTags() {
+    init();
+    return mParamTags;
+  }
+
+  public SeeTagInfo[] seeTags() {
+    init();
+    return mSeeTags;
+  }
+
+  public ThrowsTagInfo[] throwsTags() {
+    init();
+    return mThrowsTags;
+  }
+
+  public TagInfo[] returnTags() {
+    init();
+    return mReturnTags;
+  }
+
+  public TagInfo[] deprecatedTags() {
+    init();
+    return mDeprecatedTags;
+  }
+
+  public TagInfo[] undeprecateTags() {
+    init();
+    return mUndeprecateTags;
+  }
+
+  public AttrTagInfo[] attrTags() {
+    init();
+    return mAttrTags;
+  }
+
+  public TagInfo[] briefTags() {
+    init();
+    return mBriefTags;
+  }
+
+  public boolean isHidden() {
+    if (mHidden != -1) {
+      return mHidden != 0;
+    } else {
+      if (Doclava.checkLevel(Doclava.SHOW_HIDDEN)) {
+        mHidden = 0;
+        return false;
+      }
+      boolean b = mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0;
+      mHidden = b ? 1 : 0;
+      return b;
+    }
+  }
+
+  public boolean isDocOnly() {
+    if (mDocOnly != -1) {
+      return mDocOnly != 0;
+    } else {
+      boolean b = (mText != null) && (mText.indexOf("@doconly") >= 0);
+      mDocOnly = b ? 1 : 0;
+      return b;
+    }
+  }
+  
+  public boolean isDeprecated() {
+    if (mDeprecated != -1) {
+      return mDeprecated != 0;
+    } else {
+      boolean b = (mText != null) && (mText.indexOf("@deprecated") >= 0);
+      mDeprecated = b ? 1 : 0;
+      return b;
+    }
+  }
+
+  private void init() {
+    if (!mInitialized) {
+      initImpl();
+    }
+  }
+
+  private void initImpl() {
+    isHidden();
+    isDocOnly();
+    isDeprecated();
+
+    // Don't bother parsing text if we aren't generating documentation.
+    if (Doclava.generatingDocs()) {
+      parseRegex(mText);
+      parseBriefTags();
+    } else {
+      // Forces methods to be recognized by findOverriddenMethods in MethodInfo.
+      mInlineTagsList.add(new TextTagInfo("Text", "Text", mText,
+          SourcePositionInfo.add(mPosition, mText, 0)));
+    }
+
+    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()]);
+
+    mParamTagsList = null;
+    mSeeTagsList = null;
+    mThrowsTagsList = null;
+    mReturnTagsList = null;
+    mDeprecatedTagsList = null;
+    mUndeprecateTagsList = null;
+    mAttrTagsList = null;
+    mBriefTagsList = null;
+  }
+
+  boolean mInitialized;
+  int mHidden = -1;
+  int mDocOnly = -1;
+  int mDeprecated = -1;
+  String mText;
+  ContainerInfo mBase;
+  SourcePositionInfo mPosition;
+  int mLine = 1;
+
+  TagInfo[] mInlineTags;
+  TagInfo[] mTags;
+  ParamTagInfo[] mParamTags;
+  SeeTagInfo[] mSeeTags;
+  ThrowsTagInfo[] mThrowsTags;
+  TagInfo[] mBriefTags;
+  TagInfo[] mReturnTags;
+  TagInfo[] mDeprecatedTags;
+  TagInfo[] mUndeprecateTags;
+  AttrTagInfo[] mAttrTags;
+
+  ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
+  ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
+  ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>();
+  ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>();
+  ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>();
+  ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>();
+  ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>();
+  ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
+  ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
+  ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();
+
+
+}
diff --git a/src/com/google/doclava/ConstructorInfo.java b/src/com/google/doclava/ConstructorInfo.java
new file mode 100644
index 0000000..0f2c2fd
--- /dev/null
+++ b/src/com/google/doclava/ConstructorInfo.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.doclava.apicheck.AbstractMethodInfo;
+
+import java.util.*;
+
+public class ConstructorInfo implements AbstractMethodInfo {
+
+  private boolean mIsVarargs;
+  private String mName;
+  private String mType;
+  private boolean mIsStatic;
+  private boolean mIsFinal;
+  private String mDeprecated;
+  private boolean mIsDeprecated;
+  private String mScope;
+  private List<String> mExceptions;
+  private List<ParameterInfo> mParameters;
+  private SourcePositionInfo mSourcePosition;
+  private ClassInfo mClass;
+
+  public ConstructorInfo(String name, String type, boolean isStatic, boolean isFinal,
+      String deprecated, String scope, SourcePositionInfo pos, ClassInfo clazz) {
+    mName = name;
+    mType = type;
+    mIsStatic = isStatic;
+    mIsFinal = isFinal;
+    mDeprecated = deprecated;
+    mIsDeprecated = "deprecated".equals(deprecated);
+    mScope = scope;
+    mExceptions = new ArrayList<String>();
+    mParameters = new ArrayList<ParameterInfo>();
+    mSourcePosition = pos;
+    mClass = clazz;
+  }
+  
+  public void setDeprecated(boolean deprecated) {
+    mIsDeprecated = deprecated;
+  }
+
+  public void addParameter(ParameterInfo pInfo) {
+    mParameters.add(pInfo);
+  }
+
+  public void addException(String exec) {
+    mExceptions.add(exec);
+  }
+
+  public String getHashableName() {
+    StringBuilder result = new StringBuilder();
+    result.append(name());
+    for (ParameterInfo pInfo : mParameters) {
+      result.append(":").append(pInfo.typeName());
+    }
+    return result.toString();
+  }
+
+  public SourcePositionInfo position() {
+    return mSourcePosition;
+  }
+
+  public String name() {
+    return mName;
+  }
+
+  public String qualifiedName() {
+    String baseName = (mClass != null) ? (mClass.qualifiedName() + ".") : "";
+    return baseName + name();
+  }
+
+  public String prettySignature() {
+    String params = "";
+    for (ParameterInfo pInfo : mParameters) {
+      if (params.length() > 0) {
+        params += ", ";
+      }
+      params += pInfo.typeName();
+    }
+    return qualifiedName() + '(' + params + ')';
+  }
+
+  public boolean isConsistent(ConstructorInfo mInfo) {
+    boolean consistent = true;
+
+    if (mIsFinal != mInfo.mIsFinal) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_FINAL, mInfo.position(), "Constructor " + mInfo.qualifiedName()
+          + " has changed 'final' qualifier");
+    }
+
+    if (mIsStatic != mInfo.mIsStatic) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_FINAL, mInfo.position(), "Constructor " + mInfo.qualifiedName()
+          + " has changed 'static' qualifier");
+    }
+
+    if (!mScope.equals(mInfo.mScope)) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_SCOPE, mInfo.position(), "Constructor " + mInfo.qualifiedName()
+          + " changed scope from " + mScope + " to " + mInfo.mScope);
+    }
+
+    if (!mIsDeprecated == mInfo.mIsDeprecated) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), "Constructor "
+          + mInfo.qualifiedName() + " has changed deprecation state");
+    }
+
+    for (String exec : mExceptions) {
+      if (!mInfo.mExceptions.contains(exec)) {
+        Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Constructor "
+            + mInfo.qualifiedName() + " no longer throws exception " + exec);
+        consistent = false;
+      }
+    }
+
+    for (String exec : mInfo.mExceptions) {
+      if (!mExceptions.contains(exec)) {
+        Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Constructor "
+            + mInfo.qualifiedName() + " added thrown exception " + exec);
+        consistent = false;
+      }
+    }
+
+    return consistent;
+  }
+
+  @Override
+  public void setVarargs(boolean varargs) {
+    mIsVarargs = varargs;
+  }
+
+  public boolean isVarArgs() {
+    return mIsVarargs;
+  }
+
+}
diff --git a/src/com/google/doclava/ContainerInfo.java b/src/com/google/doclava/ContainerInfo.java
new file mode 100644
index 0000000..bde9506
--- /dev/null
+++ b/src/com/google/doclava/ContainerInfo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public interface ContainerInfo {
+  public String qualifiedName();
+
+  public boolean checkLevel();
+}
diff --git a/src/com/google/doclava/Converter.java b/src/com/google/doclava/Converter.java
new file mode 100644
index 0000000..06b7acb
--- /dev/null
+++ b/src/com/google/doclava/Converter.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.sun.javadoc.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class Converter {
+  private static RootDoc root;
+
+  public static void makeInfo(RootDoc r) {
+    root = r;
+
+    int N, i;
+
+    // create the objects
+    ClassDoc[] classDocs = r.classes();
+    N = classDocs.length;
+    for (i = 0; i < N; i++) {
+      Converter.obtainClass(classDocs[i]);
+    }
+    ArrayList<ClassInfo> classesNeedingInit2 = new ArrayList<ClassInfo>();
+    // fill in the fields that reference other classes
+    while (mClassesNeedingInit.size() > 0) {
+      i = mClassesNeedingInit.size() - 1;
+      ClassNeedingInit clni = mClassesNeedingInit.get(i);
+      mClassesNeedingInit.remove(i);
+
+      initClass(clni.c, clni.cl);
+      classesNeedingInit2.add(clni.cl);
+    }
+    mClassesNeedingInit = null;
+    for (ClassInfo cl : classesNeedingInit2) {
+      cl.init2();
+    }
+
+    finishAnnotationValueInit();
+
+    // fill in the "root" stuff
+    mRootClasses = Converter.convertClasses(r.classes());
+  }
+
+  private static ClassInfo[] mRootClasses;
+
+  public static ClassInfo[] rootClasses() {
+    return mRootClasses;
+  }
+
+  public static ClassInfo[] allClasses() {
+    return (ClassInfo[]) mClasses.all();
+  }
+
+  private static void initClass(ClassDoc c, ClassInfo cl) {
+    MethodDoc[] annotationElements;
+    if (c instanceof AnnotationTypeDoc) {
+      annotationElements = ((AnnotationTypeDoc) c).elements();
+    } else {
+      annotationElements = new MethodDoc[0];
+    }
+    cl.init(Converter.obtainType(c), Converter.convertClasses(c.interfaces()), Converter
+        .convertTypes(c.interfaceTypes()), Converter.convertClasses(c.innerClasses()), Converter
+        .convertMethods(c.constructors(false)), Converter.convertMethods(c.methods(false)),
+        Converter.convertMethods(annotationElements), Converter.convertFields(c.fields(false)),
+        Converter.convertFields(c.enumConstants()), Converter.obtainPackage(c.containingPackage()),
+        Converter.obtainClass(c.containingClass()), Converter.obtainClass(c.superclass()),
+        Converter.obtainType(c.superclassType()), Converter.convertAnnotationInstances(c
+            .annotations()));
+    cl.setHiddenMethods(Converter.getHiddenMethods(c.methods(false)));
+    cl.setNonWrittenConstructors(Converter.convertNonWrittenConstructors(c.constructors(false)));
+    cl.init3(Converter.convertTypes(c.typeParameters()), Converter.convertClasses(c
+        .innerClasses(false)));
+  }
+
+  public static ClassInfo obtainClass(String className) {
+    return Converter.obtainClass(root.classNamed(className));
+  }
+
+  public static PackageInfo obtainPackage(String packageName) {
+    return Converter.obtainPackage(root.packageNamed(packageName));
+  }
+
+  private static TagInfo convertTag(Tag tag) {
+    return new TextTagInfo(tag.name(), tag.kind(), tag.text(), Converter.convertSourcePosition(tag
+        .position()));
+  }
+
+  private static ThrowsTagInfo convertThrowsTag(ThrowsTag tag, ContainerInfo base) {
+    return new ThrowsTagInfo(tag.name(), tag.text(), tag.kind(), Converter.obtainClass(tag
+        .exception()), tag.exceptionComment(), base, Converter
+        .convertSourcePosition(tag.position()));
+  }
+
+  private static ParamTagInfo convertParamTag(ParamTag tag, ContainerInfo base) {
+    return new ParamTagInfo(tag.name(), tag.kind(), tag.text(), tag.isTypeParameter(), tag
+        .parameterComment(), tag.parameterName(), base, Converter.convertSourcePosition(tag
+        .position()));
+  }
+
+  private static SeeTagInfo convertSeeTag(SeeTag tag, ContainerInfo base) {
+    return new SeeTagInfo(tag.name(), tag.kind(), tag.text(), base, Converter
+        .convertSourcePosition(tag.position()));
+  }
+
+  private static SourcePositionInfo convertSourcePosition(SourcePosition sp) {
+    if (sp == null) {
+      return null;
+    }
+    return new SourcePositionInfo(sp.file().toString(), sp.line(), sp.column());
+  }
+
+  public static TagInfo[] convertTags(Tag[] tags, ContainerInfo base) {
+    int len = tags.length;
+    TagInfo[] out = new TagInfo[len];
+    for (int i = 0; i < len; i++) {
+      Tag t = tags[i];
+      /*
+       * System.out.println("Tag name='" + t.name() + "' kind='" + t.kind() + "'");
+       */
+      if (t instanceof SeeTag) {
+        out[i] = Converter.convertSeeTag((SeeTag) t, base);
+      } else if (t instanceof ThrowsTag) {
+        out[i] = Converter.convertThrowsTag((ThrowsTag) t, base);
+      } else if (t instanceof ParamTag) {
+        out[i] = Converter.convertParamTag((ParamTag) t, base);
+      } else {
+        out[i] = Converter.convertTag(t);
+      }
+    }
+    return out;
+  }
+
+  public static ClassInfo[] convertClasses(ClassDoc[] classes) {
+    if (classes == null) return null;
+    int N = classes.length;
+    ClassInfo[] result = new ClassInfo[N];
+    for (int i = 0; i < N; i++) {
+      result[i] = Converter.obtainClass(classes[i]);
+    }
+    return result;
+  }
+
+  private static ParameterInfo convertParameter(Parameter p, SourcePosition pos, boolean isVarArg) {
+    if (p == null) return null;
+    ParameterInfo pi =
+        new ParameterInfo(p.name(), p.typeName(), Converter.obtainType(p.type()), isVarArg,
+          Converter.convertSourcePosition(pos));
+    return pi;
+  }
+
+  private static ParameterInfo[] convertParameters(Parameter[] p, ExecutableMemberDoc m) {
+    SourcePosition pos = m.position();
+    int len = p.length;
+    ParameterInfo[] q = new ParameterInfo[len];
+    for (int i = 0; i < len; i++) {
+      boolean isVarArg = (m.isVarArgs() && i == len - 1);
+      q[i] = Converter.convertParameter(p[i], pos, isVarArg);
+    }
+    return q;
+  }
+
+  private static TypeInfo[] convertTypes(Type[] p) {
+    if (p == null) return null;
+    int len = p.length;
+    TypeInfo[] q = new TypeInfo[len];
+    for (int i = 0; i < len; i++) {
+      q[i] = Converter.obtainType(p[i]);
+    }
+    return q;
+  }
+
+  private Converter() {}
+
+  private static class ClassNeedingInit {
+    ClassNeedingInit(ClassDoc c, ClassInfo cl) {
+      this.c = c;
+      this.cl = cl;
+    }
+
+    ClassDoc c;
+    ClassInfo cl;
+  }
+
+  private static ArrayList<ClassNeedingInit> mClassesNeedingInit =
+      new ArrayList<ClassNeedingInit>();
+
+  static ClassInfo obtainClass(ClassDoc o) {
+    return (ClassInfo) mClasses.obtain(o);
+  }
+
+  private static Cache mClasses = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      ClassDoc c = (ClassDoc) o;
+      ClassInfo cl =
+          new ClassInfo(c, c.getRawCommentText(), Converter.convertSourcePosition(c.position()), c
+              .isPublic(), c.isProtected(), c.isPackagePrivate(), c.isPrivate(), c.isStatic(), c
+              .isInterface(), c.isAbstract(), c.isOrdinaryClass(), c.isException(), c.isError(), c
+              .isEnum(), (c instanceof AnnotationTypeDoc), c.isFinal(), c.isIncluded(), c.name(), c
+              .qualifiedName(), c.qualifiedTypeName(), c.isPrimitive());
+      if (mClassesNeedingInit != null) {
+        mClassesNeedingInit.add(new ClassNeedingInit(c, cl));
+      }
+      return cl;
+    }
+
+    @Override
+    protected void made(Object o, Object r) {
+      if (mClassesNeedingInit == null) {
+        initClass((ClassDoc) o, (ClassInfo) r);
+        ((ClassInfo) r).init2();
+      }
+    }
+
+    @Override
+    ClassInfo[] all() {
+      return mCache.values().toArray(new ClassInfo[mCache.size()]);
+    }
+  };
+
+  private static MethodInfo[] getHiddenMethods(MethodDoc[] methods) {
+    if (methods == null) return null;
+    ArrayList<MethodInfo> out = new ArrayList<MethodInfo>();
+    int N = methods.length;
+    for (int i = 0; i < N; i++) {
+      MethodInfo m = Converter.obtainMethod(methods[i]);
+      // System.out.println(m.toString() + ": ");
+      // for (TypeInfo ti : m.getTypeParameters()){
+      // if (ti.asClassInfo() != null){
+      // System.out.println(" " +ti.asClassInfo().toString());
+      // } else {
+      // System.out.println(" null");
+      // }
+      // }
+      if (m.isHidden()) {
+        out.add(m);
+      }
+    }
+    return out.toArray(new MethodInfo[out.size()]);
+  }
+
+  /**
+   * Convert MethodDoc[] into MethodInfo[]. Also filters according to the -private, -public option,
+   * because the filtering doesn't seem to be working in the ClassDoc.constructors(boolean) call.
+   */
+  private static MethodInfo[] convertMethods(MethodDoc[] methods) {
+    if (methods == null) return null;
+    ArrayList<MethodInfo> out = new ArrayList<MethodInfo>();
+    int N = methods.length;
+    for (int i = 0; i < N; i++) {
+      MethodInfo m = Converter.obtainMethod(methods[i]);
+      // System.out.println(m.toString() + ": ");
+      // for (TypeInfo ti : m.getTypeParameters()){
+      // if (ti.asClassInfo() != null){
+      // System.out.println(" " +ti.asClassInfo().toString());
+      // } else {
+      // System.out.println(" null");
+      // }
+      // }
+      if (m.checkLevel()) {
+        out.add(m);
+      }
+    }
+    return out.toArray(new MethodInfo[out.size()]);
+  }
+
+  private static MethodInfo[] convertMethods(ConstructorDoc[] methods) {
+    if (methods == null) return null;
+    ArrayList<MethodInfo> out = new ArrayList<MethodInfo>();
+    int N = methods.length;
+    for (int i = 0; i < N; i++) {
+      MethodInfo m = Converter.obtainMethod(methods[i]);
+      if (m.checkLevel()) {
+        out.add(m);
+      }
+    }
+    return out.toArray(new MethodInfo[out.size()]);
+  }
+
+  private static MethodInfo[] convertNonWrittenConstructors(ConstructorDoc[] methods) {
+    if (methods == null) return null;
+    ArrayList<MethodInfo> out = new ArrayList<MethodInfo>();
+    int N = methods.length;
+    for (int i = 0; i < N; i++) {
+      MethodInfo m = Converter.obtainMethod(methods[i]);
+      if (!m.checkLevel()) {
+        out.add(m);
+      }
+    }
+    return out.toArray(new MethodInfo[out.size()]);
+  }
+
+  private static MethodInfo obtainMethod(MethodDoc o) {
+    return (MethodInfo) mMethods.obtain(o);
+  }
+
+  private static MethodInfo obtainMethod(ConstructorDoc o) {
+    return (MethodInfo) mMethods.obtain(o);
+  }
+
+  private static Cache mMethods = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      if (o instanceof AnnotationTypeElementDoc) {
+        AnnotationTypeElementDoc m = (AnnotationTypeElementDoc) o;
+        MethodInfo result =
+            new MethodInfo(m.getRawCommentText(), Converter.convertTypes(m.typeParameters()), m
+                .name(), m.signature(), Converter.obtainClass(m.containingClass()), Converter
+                .obtainClass(m.containingClass()), m.isPublic(), m.isProtected(), m
+                .isPackagePrivate(), m.isPrivate(), m.isFinal(), m.isStatic(), m.isSynthetic(), m
+                .isAbstract(), m.isSynchronized(), m.isNative(), true, "annotationElement", m
+                .flatSignature(), Converter.obtainMethod(m.overriddenMethod()), Converter
+                .obtainType(m.returnType()), Converter.convertParameters(m.parameters(), m),
+                Converter.convertClasses(m.thrownExceptions()), Converter.convertSourcePosition(m
+                    .position()), Converter.convertAnnotationInstances(m.annotations()));
+        result.setVarargs(m.isVarArgs());
+        result.init(Converter.obtainAnnotationValue(m.defaultValue(), result));
+        return result;
+      } else if (o instanceof MethodDoc) {
+        MethodDoc m = (MethodDoc) o;
+        MethodInfo result =
+            new MethodInfo(m.getRawCommentText(), Converter.convertTypes(m.typeParameters()), m
+                .name(), m.signature(), Converter.obtainClass(m.containingClass()), Converter
+                .obtainClass(m.containingClass()), m.isPublic(), m.isProtected(), m
+                .isPackagePrivate(), m.isPrivate(), m.isFinal(), m.isStatic(), m.isSynthetic(), m
+                .isAbstract(), m.isSynchronized(), m.isNative(), false, "method",
+                m.flatSignature(), Converter.obtainMethod(m.overriddenMethod()), Converter
+                    .obtainType(m.returnType()), Converter.convertParameters(m.parameters(), m),
+                Converter.convertClasses(m.thrownExceptions()), Converter.convertSourcePosition(m
+                    .position()), Converter.convertAnnotationInstances(m.annotations()));
+        result.setVarargs(m.isVarArgs());
+        result.init(null);
+        return result;
+      } else {
+        ConstructorDoc m = (ConstructorDoc) o;
+        MethodInfo result =
+            new MethodInfo(m.getRawCommentText(), Converter.convertTypes(m.typeParameters()), m
+                .name(), m.signature(), Converter.obtainClass(m.containingClass()), Converter
+                .obtainClass(m.containingClass()), m.isPublic(), m.isProtected(), m
+                .isPackagePrivate(), m.isPrivate(), m.isFinal(), m.isStatic(), m.isSynthetic(),
+                false, m.isSynchronized(), m.isNative(), false, "constructor", m.flatSignature(),
+                null, null, Converter.convertParameters(m.parameters(), m), Converter
+                    .convertClasses(m.thrownExceptions()), Converter.convertSourcePosition(m
+                    .position()), Converter.convertAnnotationInstances(m.annotations()));
+        result.setVarargs(m.isVarArgs());
+        result.init(null);
+        return result;
+      }
+    }
+  };
+
+
+  private static FieldInfo[] convertFields(FieldDoc[] fields) {
+    if (fields == null) return null;
+    ArrayList<FieldInfo> out = new ArrayList<FieldInfo>();
+    int N = fields.length;
+    for (int i = 0; i < N; i++) {
+      FieldInfo f = Converter.obtainField(fields[i]);
+      if (f.checkLevel()) {
+        out.add(f);
+      }
+    }
+    return out.toArray(new FieldInfo[out.size()]);
+  }
+
+  private static FieldInfo obtainField(FieldDoc o) {
+    return (FieldInfo) mFields.obtain(o);
+  }
+
+  private static FieldInfo obtainField(ConstructorDoc o) {
+    return (FieldInfo) mFields.obtain(o);
+  }
+
+  private static Cache mFields = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      FieldDoc f = (FieldDoc) o;
+      return new FieldInfo(f.name(), Converter.obtainClass(f.containingClass()), Converter
+          .obtainClass(f.containingClass()), f.isPublic(), f.isProtected(), f.isPackagePrivate(), f
+          .isPrivate(), f.isFinal(), f.isStatic(), f.isTransient(), f.isVolatile(),
+          f.isSynthetic(), Converter.obtainType(f.type()), f.getRawCommentText(),
+          f.constantValue(), Converter.convertSourcePosition(f.position()), Converter
+              .convertAnnotationInstances(f.annotations()));
+    }
+  };
+
+  private static PackageInfo obtainPackage(PackageDoc o) {
+    return (PackageInfo) mPackagees.obtain(o);
+  }
+
+  private static Cache mPackagees = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      PackageDoc p = (PackageDoc) o;
+      return new PackageInfo(p, p.name(), Converter.convertSourcePosition(p.position()));
+    }
+  };
+
+  private static TypeInfo obtainType(Type o) {
+    return (TypeInfo) mTypes.obtain(o);
+  }
+
+  private static Cache mTypes = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      Type t = (Type) o;
+      String simpleTypeName;
+      if (t instanceof ClassDoc) {
+        simpleTypeName = ((ClassDoc) t).name();
+      } else {
+        simpleTypeName = t.simpleTypeName();
+      }
+      TypeInfo ti =
+          new TypeInfo(t.isPrimitive(), t.dimension(), simpleTypeName, t.qualifiedTypeName(),
+              Converter.obtainClass(t.asClassDoc()));
+      return ti;
+    }
+
+    @Override
+    protected void made(Object o, Object r) {
+      Type t = (Type) o;
+      TypeInfo ti = (TypeInfo) r;
+      if (t.asParameterizedType() != null) {
+        ti.setTypeArguments(Converter.convertTypes(t.asParameterizedType().typeArguments()));
+      } else if (t instanceof ClassDoc) {
+        ti.setTypeArguments(Converter.convertTypes(((ClassDoc) t).typeParameters()));
+      } else if (t.asTypeVariable() != null) {
+        ti.setBounds(null, Converter.convertTypes((t.asTypeVariable().bounds())));
+        ti.setIsTypeVariable(true);
+      } else if (t.asWildcardType() != null) {
+        ti.setIsWildcard(true);
+        ti.setBounds(Converter.convertTypes(t.asWildcardType().superBounds()), Converter
+            .convertTypes(t.asWildcardType().extendsBounds()));
+      }
+    }
+
+    @Override
+    protected Object keyFor(Object o) {
+      Type t = (Type) o;
+      String keyString = o.getClass().getName() + "/" + o.toString() + "/";
+      if (t.asParameterizedType() != null) {
+        keyString += t.asParameterizedType().toString() + "/";
+        if (t.asParameterizedType().typeArguments() != null) {
+          for (Type ty : t.asParameterizedType().typeArguments()) {
+            keyString += ty.toString() + "/";
+          }
+        }
+      } else {
+        keyString += "NoParameterizedType//";
+      }
+      if (t.asTypeVariable() != null) {
+        keyString += t.asTypeVariable().toString() + "/";
+        if (t.asTypeVariable().bounds() != null) {
+          for (Type ty : t.asTypeVariable().bounds()) {
+            keyString += ty.toString() + "/";
+          }
+        }
+      } else {
+        keyString += "NoTypeVariable//";
+      }
+      if (t.asWildcardType() != null) {
+        keyString += t.asWildcardType().toString() + "/";
+        if (t.asWildcardType().superBounds() != null) {
+          for (Type ty : t.asWildcardType().superBounds()) {
+            keyString += ty.toString() + "/";
+          }
+        }
+        if (t.asWildcardType().extendsBounds() != null) {
+          for (Type ty : t.asWildcardType().extendsBounds()) {
+            keyString += ty.toString() + "/";
+          }
+        }
+      } else {
+        keyString += "NoWildCardType//";
+      }
+
+      return keyString;
+    }
+  };
+  
+  public static TypeInfo obtainTypeFromString(String type) {
+    return (TypeInfo) mTypesFromString.obtain(type);
+  }
+  
+  private static final Cache mTypesFromString = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      String name = (String) o;
+      return new TypeInfo(name);
+    }
+
+    @Override
+    protected void made(Object o, Object r) {
+      
+    }
+
+    @Override
+    protected Object keyFor(Object o) {
+      return o;
+    }
+  };
+
+  private static MemberInfo obtainMember(MemberDoc o) {
+    return (MemberInfo) mMembers.obtain(o);
+  }
+
+  private static Cache mMembers = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      if (o instanceof MethodDoc) {
+        return Converter.obtainMethod((MethodDoc) o);
+      } else if (o instanceof ConstructorDoc) {
+        return Converter.obtainMethod((ConstructorDoc) o);
+      } else if (o instanceof FieldDoc) {
+        return Converter.obtainField((FieldDoc) o);
+      } else {
+        return null;
+      }
+    }
+  };
+
+  private static AnnotationInstanceInfo[] convertAnnotationInstances(AnnotationDesc[] orig) {
+    int len = orig.length;
+    AnnotationInstanceInfo[] out = new AnnotationInstanceInfo[len];
+    for (int i = 0; i < len; i++) {
+      out[i] = Converter.obtainAnnotationInstance(orig[i]);
+    }
+    return out;
+  }
+
+
+  private static AnnotationInstanceInfo obtainAnnotationInstance(AnnotationDesc o) {
+    return (AnnotationInstanceInfo) mAnnotationInstances.obtain(o);
+  }
+
+  private static Cache mAnnotationInstances = new Cache() {
+    @Override
+    protected Object make(Object o) {
+      AnnotationDesc a = (AnnotationDesc) o;
+      ClassInfo annotationType = Converter.obtainClass(a.annotationType());
+      AnnotationDesc.ElementValuePair[] ev = a.elementValues();
+      AnnotationValueInfo[] elementValues = new AnnotationValueInfo[ev.length];
+      for (int i = 0; i < ev.length; i++) {
+        elementValues[i] =
+            obtainAnnotationValue(ev[i].value(), Converter.obtainMethod(ev[i].element()));
+      }
+      return new AnnotationInstanceInfo(annotationType, elementValues);
+    }
+  };
+
+
+  private abstract static class Cache {
+    void put(Object key, Object value) {
+      mCache.put(key, value);
+    }
+
+    Object obtain(Object o) {
+      if (o == null) {
+        return null;
+      }
+      Object k = keyFor(o);
+      Object r = mCache.get(k);
+      if (r == null) {
+        r = make(o);
+        mCache.put(k, r);
+        made(o, r);
+      }
+      return r;
+    }
+
+    protected HashMap<Object, Object> mCache = new HashMap<Object, Object>();
+
+    protected abstract Object make(Object o);
+
+    protected void made(Object o, Object r) {}
+
+    protected Object keyFor(Object o) {
+      return o;
+    }
+
+    Object[] all() {
+      return null;
+    }
+  }
+
+  // annotation values
+  private static HashMap<AnnotationValue, AnnotationValueInfo> mAnnotationValues =
+      new HashMap<AnnotationValue, AnnotationValueInfo>();
+  private static HashSet<AnnotationValue> mAnnotationValuesNeedingInit =
+      new HashSet<AnnotationValue>();
+
+  private static AnnotationValueInfo obtainAnnotationValue(AnnotationValue o, MethodInfo element) {
+    if (o == null) {
+      return null;
+    }
+    AnnotationValueInfo v = mAnnotationValues.get(o);
+    if (v != null) return v;
+    v = new AnnotationValueInfo(element);
+    mAnnotationValues.put(o, v);
+    if (mAnnotationValuesNeedingInit != null) {
+      mAnnotationValuesNeedingInit.add(o);
+    } else {
+      initAnnotationValue(o, v);
+    }
+    return v;
+  }
+
+  private static void initAnnotationValue(AnnotationValue o, AnnotationValueInfo v) {
+    Object orig = o.value();
+    Object converted;
+    if (orig instanceof Type) {
+      // class literal
+      converted = Converter.obtainType((Type) orig);
+    } else if (orig instanceof FieldDoc) {
+      // enum constant
+      converted = Converter.obtainField((FieldDoc) orig);
+    } else if (orig instanceof AnnotationDesc) {
+      // annotation instance
+      converted = Converter.obtainAnnotationInstance((AnnotationDesc) orig);
+    } else if (orig instanceof AnnotationValue[]) {
+      AnnotationValue[] old = (AnnotationValue[]) orig;
+      AnnotationValueInfo[] array = new AnnotationValueInfo[old.length];
+      for (int i = 0; i < array.length; i++) {
+        array[i] = Converter.obtainAnnotationValue(old[i], null);
+      }
+      converted = array;
+    } else {
+      converted = orig;
+    }
+    v.init(converted);
+  }
+
+  private static void finishAnnotationValueInit() {
+    int depth = 0;
+    while (mAnnotationValuesNeedingInit.size() > 0) {
+      HashSet<AnnotationValue> set = mAnnotationValuesNeedingInit;
+      mAnnotationValuesNeedingInit = new HashSet<AnnotationValue>();
+      for (AnnotationValue o : set) {
+        AnnotationValueInfo v = mAnnotationValues.get(o);
+        initAnnotationValue(o, v);
+      }
+      depth++;
+    }
+    mAnnotationValuesNeedingInit = null;
+  }
+}
diff --git a/src/com/google/doclava/DocFile.java b/src/com/google/doclava/DocFile.java
new file mode 100644
index 0000000..ab0a4fd
--- /dev/null
+++ b/src/com/google/doclava/DocFile.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.io.*;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+
+public class DocFile {
+  private static final Pattern LINE = Pattern.compile("(.*)[\r]?\n", Pattern.MULTILINE);
+  private static final Pattern PROP = Pattern.compile("([^=]+)=(.*)");
+
+  public static String readFile(String filename) {
+    try {
+      File f = new File(filename);
+      int length = (int) f.length();
+      FileInputStream is = new FileInputStream(f);
+      InputStreamReader reader = new InputStreamReader(is, "UTF-8");
+      char[] buf = new char[length];
+      int index = 0;
+      int amt;
+      while (true) {
+        amt = reader.read(buf, index, length - index);
+
+        if (amt < 1) {
+          break;
+        }
+
+        index += amt;
+      }
+      return new String(buf, 0, index);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  public static void writePage(String docfile, String relative, String outfile) {
+    Data hdf = Doclava.makeHDF();
+
+    /*
+     * System.out.println("docfile='" + docfile + "' relative='" + relative + "'" + "' outfile='" +
+     * outfile + "'");
+     */
+
+    String filedata = readFile(docfile);
+
+    // The document is properties up until the line "@jd:body".
+    // Any blank lines are ignored.
+    int start = -1;
+    int lineno = 1;
+    Matcher lines = LINE.matcher(filedata);
+    String line = null;
+    while (lines.find()) {
+      line = lines.group(1);
+      if (line.length() > 0) {
+        if (line.equals("@jd:body")) {
+          start = lines.end();
+          break;
+        }
+        Matcher prop = PROP.matcher(line);
+        if (prop.matches()) {
+          String key = prop.group(1);
+          String value = prop.group(2);
+          hdf.setValue(key, value);
+        } else {
+          break;
+        }
+      }
+      lineno++;
+    }
+    if (start < 0) {
+      System.err.println(docfile + ":" + lineno + ": error parsing docfile");
+      if (line != null) {
+        System.err.println(docfile + ":" + lineno + ":" + line);
+      }
+      System.exit(1);
+    }
+
+    // if they asked to only be for a certain template, maybe skip it
+    String fromTemplate = hdf.getValue("template.which", "");
+    String fromPage = hdf.getValue("page.onlyfortemplate", "");
+    if (!"".equals(fromPage) && !fromTemplate.equals(fromPage)) {
+      return;
+    }
+
+    // and the actual text after that
+    String commentText = filedata.substring(start);
+
+    Comment comment = new Comment(commentText, null, new SourcePositionInfo(docfile, lineno, 1));
+    TagInfo[] tags = comment.tags();
+
+    TagInfo.makeHDF(hdf, "root.descr", tags);
+
+    hdf.setValue("commentText", commentText);
+
+    // write the page using the appropriate root template, based on the
+    // whichdoc value supplied by build
+    String fromWhichmodule = hdf.getValue("android.whichmodule", "");
+    if (fromWhichmodule.equals("online-pdk")) {
+      // leaving this in just for temporary compatibility with pdk doc
+      hdf.setValue("online-pdk", "true");
+      // add any conditional login for root template here (such as
+      // for custom left nav based on tab etc.
+      ClearPage.write(hdf, "docpage.cs", outfile);
+    } else {
+      if (outfile.indexOf("sdk/") != -1) {
+        hdf.setValue("sdk", "true");
+        if ((outfile.indexOf("index.html") != -1) || (outfile.indexOf("features.html") != -1)) {
+          ClearPage.write(hdf, "sdkpage.cs", outfile);
+        } else {
+          ClearPage.write(hdf, "docpage.cs", outfile);
+        }
+      } else if (outfile.indexOf("guide/") != -1) {
+        hdf.setValue("guide", "true");
+        ClearPage.write(hdf, "docpage.cs", outfile);
+      } else if (outfile.indexOf("resources/") != -1) {
+        hdf.setValue("resources", "true");
+        ClearPage.write(hdf, "docpage.cs", outfile);
+      } else {
+        ClearPage.write(hdf, "nosidenavpage.cs", outfile);
+      }
+    }
+  } // writePage
+}
diff --git a/src/com/google/doclava/DocInfo.java b/src/com/google/doclava/DocInfo.java
new file mode 100644
index 0000000..d1fdae1
--- /dev/null
+++ b/src/com/google/doclava/DocInfo.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public abstract class DocInfo {
+  public DocInfo(String rawCommentText, SourcePositionInfo sp) {
+    mRawCommentText = rawCommentText;
+    mPosition = sp;
+  }
+
+  /**
+   * The relative path to a web page representing this item.
+   */
+  public abstract String htmlPage();
+  
+  public boolean isHidden() {
+    return comment().isHidden();
+  }
+
+  public boolean isDocOnly() {
+    return comment().isDocOnly();
+  }
+
+  public String getRawCommentText() {
+    return mRawCommentText;
+  }
+
+  public Comment comment() {
+    if (mComment == null) {
+      mComment = new Comment(mRawCommentText, parent(), mPosition);
+    }
+    return mComment;
+  }
+
+  public SourcePositionInfo position() {
+    return mPosition;
+  }
+
+  public abstract ContainerInfo parent();
+
+  public void setSince(String since) {
+    mSince = since;
+  }
+
+  public String getSince() {
+    return mSince;
+  }
+  
+  public final void addFederatedReference(FederatedSite source) {
+    mFederatedReferences.add(source);
+  }
+  
+  public final Set<FederatedSite> getFederatedReferences() {
+    return mFederatedReferences;
+  }
+  
+  public final void setFederatedReferences(Data data, String base) {
+    int pos = 0;
+    for (FederatedSite source : getFederatedReferences()) {
+      data.setValue(base + ".federated." + pos + ".url", source.linkFor(htmlPage()));
+      data.setValue(base + ".federated." + pos + ".name", source.name());
+      pos++;
+    }
+  }
+
+  private String mRawCommentText;
+  Comment mComment;
+  SourcePositionInfo mPosition;
+  private String mSince;
+  private Set<FederatedSite> mFederatedReferences = new LinkedHashSet<FederatedSite>();
+}
diff --git a/src/com/google/doclava/Doclava.java b/src/com/google/doclava/Doclava.java
new file mode 100644
index 0000000..9489090
--- /dev/null
+++ b/src/com/google/doclava/Doclava.java
@@ -0,0 +1,1375 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.JSilver;
+import com.google.clearsilver.jsilver.data.Data;
+import com.google.clearsilver.jsilver.resourceloader.ClassResourceLoader;
+import com.google.clearsilver.jsilver.resourceloader.CompositeResourceLoader;
+import com.google.clearsilver.jsilver.resourceloader.FileSystemResourceLoader;
+import com.google.clearsilver.jsilver.resourceloader.ResourceLoader;
+
+import com.sun.javadoc.*;
+
+import java.util.*;
+import java.util.jar.JarFile;
+import java.io.*;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class Doclava {
+  private static final String SDK_CONSTANT_ANNOTATION = "android.annotation.SdkConstant";
+  private static final String SDK_CONSTANT_TYPE_ACTIVITY_ACTION =
+      "android.annotation.SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION";
+  private static final String SDK_CONSTANT_TYPE_BROADCAST_ACTION =
+      "android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION";
+  private static final String SDK_CONSTANT_TYPE_SERVICE_ACTION =
+      "android.annotation.SdkConstant.SdkConstantType.SERVICE_INTENT_ACTION";
+  private static final String SDK_CONSTANT_TYPE_CATEGORY =
+      "android.annotation.SdkConstant.SdkConstantType.INTENT_CATEGORY";
+  private static final String SDK_CONSTANT_TYPE_FEATURE =
+      "android.annotation.SdkConstant.SdkConstantType.FEATURE";
+  private static final String SDK_WIDGET_ANNOTATION = "android.annotation.Widget";
+  private static final String SDK_LAYOUT_ANNOTATION = "android.annotation.Layout";
+
+  private static final int TYPE_NONE = 0;
+  private static final int TYPE_WIDGET = 1;
+  private static final int TYPE_LAYOUT = 2;
+  private static final int TYPE_LAYOUT_PARAM = 3;
+
+  public static final int SHOW_PUBLIC = 0x00000001;
+  public static final int SHOW_PROTECTED = 0x00000003;
+  public static final int SHOW_PACKAGE = 0x00000007;
+  public static final int SHOW_PRIVATE = 0x0000000f;
+  public static final int SHOW_HIDDEN = 0x0000001f;
+
+  public static int showLevel = SHOW_PROTECTED;
+
+  public static final String javadocDir = "reference/";
+  public static String htmlExtension;
+
+  public static RootDoc root;
+  public static ArrayList<String[]> mHDFData = new ArrayList<String[]>();
+  public static Map<Character, String> escapeChars = new HashMap<Character, String>();
+  public static String title = "";
+  public static SinceTagger sinceTagger = new SinceTagger();
+  public static FederationTagger federationTagger = new FederationTagger();
+  private static boolean generatingDocs = true;
+  
+  public static JSilver jSilver = null;
+
+  public static boolean checkLevel(int level) {
+    return (showLevel & level) == level;
+  }
+  
+  /**
+   * Returns true if we are generating html reference documentation.
+   */
+  public static boolean generatingDocs() {
+    return generatingDocs;
+  }
+
+  public static boolean checkLevel(boolean pub, boolean prot, boolean pkgp, boolean priv,
+      boolean hidden) {
+    int level = 0;
+    if (hidden && !checkLevel(SHOW_HIDDEN)) {
+      return false;
+    }
+    if (pub && checkLevel(SHOW_PUBLIC)) {
+      return true;
+    }
+    if (prot && checkLevel(SHOW_PROTECTED)) {
+      return true;
+    }
+    if (pkgp && checkLevel(SHOW_PACKAGE)) {
+      return true;
+    }
+    if (priv && checkLevel(SHOW_PRIVATE)) {
+      return true;
+    }
+    return false;
+  }
+
+  public static void main(String[] args) {
+    com.sun.tools.javadoc.Main.execute(args);
+  }
+
+  public static boolean start(RootDoc r) {
+    String keepListFile = null;
+    String proofreadFile = null;
+    String todoFile = null;
+    String sdkValuePath = null;
+    ArrayList<SampleCode> sampleCodes = new ArrayList<SampleCode>();
+    String stubsDir = null;
+    // Create the dependency graph for the stubs directory
+    boolean offlineMode = false;
+    String apiFile = null;
+    String debugStubsFile = "";
+    HashSet<String> stubPackages = null;
+
+    root = r;
+
+    String[][] options = r.options();
+    for (String[] a : options) {
+      if (a[0].equals("-d")) {
+        ClearPage.outputDir = a[1];
+      } else if (a[0].equals("-templatedir")) {
+        ClearPage.addTemplateDir(a[1]);
+      } else if (a[0].equals("-hdf")) {
+        mHDFData.add(new String[] {a[1], a[2]});
+      } else if (a[0].equals("-toroot")) {
+        ClearPage.toroot = a[1];
+      } else if (a[0].equals("-samplecode")) {
+        sampleCodes.add(new SampleCode(a[1], a[2], a[3]));
+      } else if (a[0].equals("-htmldir")) {
+        ClearPage.htmlDir = a[1];
+      } else if (a[0].equals("-title")) {
+        Doclava.title = a[1];
+      } else if (a[0].equals("-werror")) {
+        Errors.setWarningsAreErrors(true);
+      } else if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) {
+        try {
+          int level = -1;
+          if (a[0].equals("-error")) {
+            level = Errors.ERROR;
+          } else if (a[0].equals("-warning")) {
+            level = Errors.WARNING;
+          } else if (a[0].equals("-hide")) {
+            level = Errors.HIDDEN;
+          }
+          Errors.setErrorLevel(Integer.parseInt(a[1]), level);
+        } catch (NumberFormatException e) {
+          // already printed below
+          return false;
+        }
+      } else if (a[0].equals("-keeplist")) {
+        keepListFile = a[1];
+      } else if (a[0].equals("-proofread")) {
+        proofreadFile = a[1];
+      } else if (a[0].equals("-todo")) {
+        todoFile = a[1];
+      } else if (a[0].equals("-public")) {
+        showLevel = SHOW_PUBLIC;
+      } else if (a[0].equals("-protected")) {
+        showLevel = SHOW_PROTECTED;
+      } else if (a[0].equals("-package")) {
+        showLevel = SHOW_PACKAGE;
+      } else if (a[0].equals("-private")) {
+        showLevel = SHOW_PRIVATE;
+      } else if (a[0].equals("-hidden")) {
+        showLevel = SHOW_HIDDEN;
+      } else if (a[0].equals("-stubs")) {
+        stubsDir = a[1];
+      } else if (a[0].equals("-stubpackages")) {
+        stubPackages = new HashSet<String>();
+        for (String pkg : a[1].split(":")) {
+          stubPackages.add(pkg);
+        }
+      } else if (a[0].equals("-sdkvalues")) {
+        sdkValuePath = a[1];
+      } else if (a[0].equals("-apixml")) {
+        apiFile = a[1];
+      } else if (a[0].equals("-nodocs")) {
+        generatingDocs = false;
+      } else if (a[0].equals("-since")) {
+        sinceTagger.addVersion(a[1], a[2]);
+      } else if (a[0].equals("-offlinemode")) {
+        offlineMode = true;
+      } else if (a[0].equals("-federate")) {
+        try {
+          String name = a[1];
+          URL federationURL = new URL(a[2]);
+          federationTagger.addSite(name, federationURL);
+        } catch (MalformedURLException e) {
+          System.err.println("Could not parse URL for federation: " + a[1]);
+          return false;
+        }
+      }
+    }
+
+
+    // Set up the data structures
+    Converter.makeInfo(r);
+
+    if (generatingDocs) {
+      ClearPage.addBundledTemplateDir("assets/customizations");
+      ClearPage.addBundledTemplateDir("assets/templates");
+
+      List<ResourceLoader> resourceLoaders = new ArrayList<ResourceLoader>();
+      List<String> templates = ClearPage.getTemplateDirs();
+      for (String tmpl : templates) {
+        resourceLoaders.add(new FileSystemResourceLoader(tmpl));
+      }
+
+      templates = ClearPage.getBundledTemplateDirs();
+      for (String tmpl : templates) {
+        resourceLoaders.add(new ClassResourceLoader(Doclava.class, '/'+tmpl));
+      }
+
+      ResourceLoader compositeResourceLoader = new CompositeResourceLoader(resourceLoaders);
+      jSilver = new JSilver(compositeResourceLoader);
+
+      if (!Doclava.readTemplateSettings()) {
+        return false;
+      }
+
+      long startTime = System.nanoTime();
+
+      // Apply @since tags from the XML file
+      sinceTagger.tagAll(Converter.rootClasses());
+      
+      // Apply details of federated documentation
+      federationTagger.tagAll(Converter.rootClasses());
+
+      // Files for proofreading
+      if (proofreadFile != null) {
+        Proofread.initProofread(proofreadFile);
+      }
+      if (todoFile != null) {
+        TodoFile.writeTodoFile(todoFile);
+      }
+
+      // HTML Pages
+      if (ClearPage.htmlDir != null) {
+        writeHTMLPages();
+      }
+
+      writeAssets();
+
+      // Navigation tree
+      NavTree.writeNavTree(javadocDir);
+
+      // Packages Pages
+      writePackages(javadocDir + "packages" + htmlExtension);
+
+      // Classes
+      writeClassLists();
+      writeClasses();
+      writeHierarchy();
+      // writeKeywords();
+
+      // Lists for JavaScript
+      writeLists();
+      if (keepListFile != null) {
+        writeKeepList(keepListFile);
+      }
+
+      // Sample Code
+      for (SampleCode sc : sampleCodes) {
+        sc.write(offlineMode);
+      }
+
+      // Index page
+      writeIndex();
+
+      Proofread.finishProofread(proofreadFile);
+
+      if (sdkValuePath != null) {
+        writeSdkValues(sdkValuePath);
+      }
+
+      long time = System.nanoTime() - startTime;
+      System.out.println("DroidDoc took " + (time / 1000000000) + " sec. to write docs to "
+          + ClearPage.outputDir);
+    }
+
+    // Stubs
+    if (stubsDir != null || apiFile != null) {
+      Stubs.writeStubsAndXml(stubsDir, apiFile, stubPackages);
+    }
+
+    Errors.printErrors();
+    
+    return !Errors.hadError;
+  }
+
+  private static void writeIndex() {
+    Data data = makeHDF();
+    ClearPage.write(data, "index.cs", javadocDir + "index" + htmlExtension);
+  }
+
+  private static boolean readTemplateSettings() {
+    Data data = makeHDF();
+
+    // The .html extension is hard-coded in several .cs files,
+    // and so you cannot currently set it as a property.
+    htmlExtension = ".html";
+    // htmlExtension = data.getValue("template.extension", ".html");
+    int i = 0;
+    while (true) {
+      String k = data.getValue("template.escape." + i + ".key", "");
+      String v = data.getValue("template.escape." + i + ".value", "");
+      if ("".equals(k)) {
+        break;
+      }
+      if (k.length() != 1) {
+        System.err.println("template.escape." + i + ".key must have a length of 1: " + k);
+        return false;
+      }
+      escapeChars.put(k.charAt(0), v);
+      i++;
+    }
+    return true;
+  }
+
+  public static String escape(String s) {
+    if (escapeChars.size() == 0) {
+      return s;
+    }
+    StringBuffer b = null;
+    int begin = 0;
+    final int N = s.length();
+    for (int i = 0; i < N; i++) {
+      char c = s.charAt(i);
+      String mapped = escapeChars.get(c);
+      if (mapped != null) {
+        if (b == null) {
+          b = new StringBuffer(s.length() + mapped.length());
+        }
+        if (begin != i) {
+          b.append(s.substring(begin, i));
+        }
+        b.append(mapped);
+        begin = i + 1;
+      }
+    }
+    if (b != null) {
+      if (begin != N) {
+        b.append(s.substring(begin, N));
+      }
+      return b.toString();
+    }
+    return s;
+  }
+
+  public static void setPageTitle(Data data, String title) {
+    String s = title;
+    if (Doclava.title.length() > 0) {
+      s += " - " + Doclava.title;
+    }
+    data.setValue("page.title", s);
+  }
+
+
+  public static LanguageVersion languageVersion() {
+    return LanguageVersion.JAVA_1_5;
+  }
+
+
+  public static int optionLength(String option) {
+    if (option.equals("-d")) {
+      return 2;
+    }
+    if (option.equals("-templatedir")) {
+      return 2;
+    }
+    if (option.equals("-hdf")) {
+      return 3;
+    }
+    if (option.equals("-toroot")) {
+      return 2;
+    }
+    if (option.equals("-samplecode")) {
+      return 4;
+    }
+    if (option.equals("-htmldir")) {
+      return 2;
+    }
+    if (option.equals("-title")) {
+      return 2;
+    }
+    if (option.equals("-werror")) {
+      return 1;
+    }
+    if (option.equals("-hide")) {
+      return 2;
+    }
+    if (option.equals("-warning")) {
+      return 2;
+    }
+    if (option.equals("-error")) {
+      return 2;
+    }
+    if (option.equals("-keeplist")) {
+      return 2;
+    }
+    if (option.equals("-proofread")) {
+      return 2;
+    }
+    if (option.equals("-todo")) {
+      return 2;
+    }
+    if (option.equals("-public")) {
+      return 1;
+    }
+    if (option.equals("-protected")) {
+      return 1;
+    }
+    if (option.equals("-package")) {
+      return 1;
+    }
+    if (option.equals("-private")) {
+      return 1;
+    }
+    if (option.equals("-hidden")) {
+      return 1;
+    }
+    if (option.equals("-stubs")) {
+      return 2;
+    }
+    if (option.equals("-stubpackages")) {
+      return 2;
+    }
+    if (option.equals("-sdkvalues")) {
+      return 2;
+    }
+    if (option.equals("-apixml")) {
+      return 2;
+    }
+    if (option.equals("-nodocs")) {
+      return 1;
+    }
+    if (option.equals("-since")) {
+      return 3;
+    }
+    if (option.equals("-offlinemode")) {
+      return 1;
+    }
+    if (option.equals("-federate")) {
+      return 3;
+    }
+    return 0;
+  }
+
+  public static boolean validOptions(String[][] options, DocErrorReporter r) {
+    for (String[] a : options) {
+      if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) {
+        try {
+          Integer.parseInt(a[1]);
+        } catch (NumberFormatException e) {
+          r.printError("bad -" + a[0] + " value must be a number: " + a[1]);
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+  public static Data makeHDF() {
+    Data data = jSilver.createData();
+
+    for (String[] p : mHDFData) {
+      data.setValue(p[0], p[1]);
+    }
+
+    return data;
+  }
+
+
+
+  public static Data makePackageHDF() {
+    Data data = makeHDF();
+    ClassInfo[] classes = Converter.rootClasses();
+
+    SortedMap<String, PackageInfo> sorted = new TreeMap<String, PackageInfo>();
+    for (ClassInfo cl : classes) {
+      PackageInfo pkg = cl.containingPackage();
+      String name;
+      if (pkg == null) {
+        name = "";
+      } else {
+        name = pkg.name();
+      }
+      sorted.put(name, pkg);
+    }
+
+    int i = 0;
+    for (String s : sorted.keySet()) {
+      PackageInfo pkg = sorted.get(s);
+
+      if (pkg.isHidden()) {
+        continue;
+      }
+      Boolean allHidden = true;
+      int pass = 0;
+      ClassInfo[] classesToCheck = null;
+      while (pass < 5) {
+        switch (pass) {
+          case 0:
+            classesToCheck = pkg.ordinaryClasses();
+            break;
+          case 1:
+            classesToCheck = pkg.enums();
+            break;
+          case 2:
+            classesToCheck = pkg.errors();
+            break;
+          case 3:
+            classesToCheck = pkg.exceptions();
+            break;
+          case 4:
+            classesToCheck = pkg.interfaces();
+            break;
+          default:
+            System.err.println("Error reading package: " + pkg.name());
+            break;
+        }
+        for (ClassInfo cl : classesToCheck) {
+          if (!cl.isHidden()) {
+            allHidden = false;
+            break;
+          }
+        }
+        if (!allHidden) {
+          break;
+        }
+        pass++;
+      }
+      if (allHidden) {
+        continue;
+      }
+
+      data.setValue("reference", "1");
+      data.setValue("reference.apilevels", sinceTagger.hasVersions() ? "1" : "0");
+      data.setValue("docs.packages." + i + ".name", s);
+      data.setValue("docs.packages." + i + ".link", pkg.htmlPage());
+      data.setValue("docs.packages." + i + ".since", pkg.getSince());
+      TagInfo.makeHDF(data, "docs.packages." + i + ".shortDescr", pkg.firstSentenceTags());
+      i++;
+    }
+
+    sinceTagger.writeVersionNames(data);
+    return data;
+  }
+
+  private static void writeDirectory(File dir, String relative, JSilver js) {
+    File[] files = dir.listFiles();
+    int i, count = files.length;
+    for (i = 0; i < count; i++) {
+      File f = files[i];
+      if (f.isFile()) {
+        String templ = relative + f.getName();
+        int len = templ.length();
+        if (len > 3 && ".cs".equals(templ.substring(len - 3))) {
+          Data data = makeHDF();
+          String filename = templ.substring(0, len - 3) + htmlExtension;
+          ClearPage.write(data, templ, filename, js);
+        } else if (len > 3 && ".jd".equals(templ.substring(len - 3))) {
+          String filename = templ.substring(0, len - 3) + htmlExtension;
+          DocFile.writePage(f.getAbsolutePath(), relative, filename);
+        } else {
+          ClearPage.copyFile(f, templ);
+        }
+      } else if (f.isDirectory()) {
+        writeDirectory(f, relative + f.getName() + "/", js);
+      }
+    }
+  }
+
+  public static void writeHTMLPages() {
+    File f = new File(ClearPage.htmlDir);
+    if (!f.isDirectory()) {
+      System.err.println("htmlDir not a directory: " + ClearPage.htmlDir);
+    }
+
+    ResourceLoader loader = new FileSystemResourceLoader(f);
+    JSilver js = new JSilver(loader);
+    writeDirectory(f, "", js);
+  }
+
+  public static void writeAssets() {
+    JarFile thisJar = JarUtils.jarForClass(Doclava.class, null);
+    if (thisJar != null) {
+      try {
+        List<String> templateDirs = ClearPage.getBundledTemplateDirs();
+        for (String templateDir : templateDirs) {
+          String assetsDir = templateDir + "/assets";
+          JarUtils.copyResourcesToDirectory(thisJar, assetsDir, ClearPage.outputDir + "/assets");
+        }
+      } catch (IOException e) {
+        System.err.println("Error copying assets directory.");
+        e.printStackTrace();
+        return;
+      }
+    }
+
+    List<String> templateDirs = ClearPage.getTemplateDirs();
+    for (String templateDir : templateDirs) {
+      File assets = new File(templateDir + "/assets");
+      if (assets.isDirectory()) {
+        writeDirectory(assets, "assets/", null);
+      }
+    }
+  }
+
+  public static void writeLists() {
+    Data data = makeHDF();
+
+    ClassInfo[] classes = Converter.rootClasses();
+
+    SortedMap<String, Object> sorted = new TreeMap<String, Object>();
+    for (ClassInfo cl : classes) {
+      if (cl.isHidden()) {
+        continue;
+      }
+      sorted.put(cl.qualifiedName(), cl);
+      PackageInfo pkg = cl.containingPackage();
+      String name;
+      if (pkg == null) {
+        name = "";
+      } else {
+        name = pkg.name();
+      }
+      sorted.put(name, pkg);
+    }
+
+    int i = 0;
+    for (String s : sorted.keySet()) {
+      data.setValue("docs.pages." + i + ".id", "" + i);
+      data.setValue("docs.pages." + i + ".label", s);
+
+      Object o = sorted.get(s);
+      if (o instanceof PackageInfo) {
+        PackageInfo pkg = (PackageInfo) o;
+        data.setValue("docs.pages." + i + ".link", pkg.htmlPage());
+        data.setValue("docs.pages." + i + ".type", "package");
+      } else if (o instanceof ClassInfo) {
+        ClassInfo cl = (ClassInfo) o;
+        data.setValue("docs.pages." + i + ".link", cl.htmlPage());
+        data.setValue("docs.pages." + i + ".type", "class");
+      }
+      i++;
+    }
+
+    ClearPage.write(data, "lists.cs", javadocDir + "lists.js");
+  }
+
+  public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable) {
+    if (!notStrippable.add(cl)) {
+      // slight optimization: if it already contains cl, it already contains
+      // all of cl's parents
+      return;
+    }
+    ClassInfo supr = cl.superclass();
+    if (supr != null) {
+      cantStripThis(supr, notStrippable);
+    }
+    for (ClassInfo iface : cl.interfaces()) {
+      cantStripThis(iface, notStrippable);
+    }
+  }
+
+  private static String getPrintableName(ClassInfo cl) {
+    ClassInfo containingClass = cl.containingClass();
+    if (containingClass != null) {
+      // This is an inner class.
+      String baseName = cl.name();
+      baseName = baseName.substring(baseName.lastIndexOf('.') + 1);
+      return getPrintableName(containingClass) + '$' + baseName;
+    }
+    return cl.qualifiedName();
+  }
+
+  /**
+   * Writes the list of classes that must be present in order to provide the non-hidden APIs known
+   * to javadoc.
+   * 
+   * @param filename the path to the file to write the list to
+   */
+  public static void writeKeepList(String filename) {
+    HashSet<ClassInfo> notStrippable = new HashSet<ClassInfo>();
+    ClassInfo[] all = Converter.allClasses();
+    Arrays.sort(all); // just to make the file a little more readable
+
+    // If a class is public and not hidden, then it and everything it derives
+    // from cannot be stripped. Otherwise we can strip it.
+    for (ClassInfo cl : all) {
+      if (cl.isPublic() && !cl.isHidden()) {
+        cantStripThis(cl, notStrippable);
+      }
+    }
+    PrintStream stream = null;
+    try {
+      stream = new PrintStream(filename);
+      for (ClassInfo cl : notStrippable) {
+        stream.println(getPrintableName(cl));
+      }
+    } catch (FileNotFoundException e) {
+      System.err.println("error writing file: " + filename);
+    } finally {
+      if (stream != null) {
+        stream.close();
+      }
+    }
+  }
+
+  private static PackageInfo[] sVisiblePackages = null;
+
+  public static PackageInfo[] choosePackages() {
+    if (sVisiblePackages != null) {
+      return sVisiblePackages;
+    }
+
+    ClassInfo[] classes = Converter.rootClasses();
+    SortedMap<String, PackageInfo> sorted = new TreeMap<String, PackageInfo>();
+    for (ClassInfo cl : classes) {
+      PackageInfo pkg = cl.containingPackage();
+      String name;
+      if (pkg == null) {
+        name = "";
+      } else {
+        name = pkg.name();
+      }
+      sorted.put(name, pkg);
+    }
+
+    ArrayList<PackageInfo> result = new ArrayList<PackageInfo>();
+
+    for (String s : sorted.keySet()) {
+      PackageInfo pkg = sorted.get(s);
+
+      if (pkg.isHidden()) {
+        continue;
+      }
+      Boolean allHidden = true;
+      int pass = 0;
+      ClassInfo[] classesToCheck = null;
+      while (pass < 5) {
+        switch (pass) {
+          case 0:
+            classesToCheck = pkg.ordinaryClasses();
+            break;
+          case 1:
+            classesToCheck = pkg.enums();
+            break;
+          case 2:
+            classesToCheck = pkg.errors();
+            break;
+          case 3:
+            classesToCheck = pkg.exceptions();
+            break;
+          case 4:
+            classesToCheck = pkg.interfaces();
+            break;
+          default:
+            System.err.println("Error reading package: " + pkg.name());
+            break;
+        }
+        for (ClassInfo cl : classesToCheck) {
+          if (!cl.isHidden()) {
+            allHidden = false;
+            break;
+          }
+        }
+        if (!allHidden) {
+          break;
+        }
+        pass++;
+      }
+      if (allHidden) {
+        continue;
+      }
+
+      result.add(pkg);
+    }
+
+    sVisiblePackages = result.toArray(new PackageInfo[result.size()]);
+    return sVisiblePackages;
+  }
+
+  public static void writePackages(String filename) {
+    Data data = makePackageHDF();
+
+    int i = 0;
+    for (PackageInfo pkg : choosePackages()) {
+      writePackage(pkg);
+
+      data.setValue("docs.packages." + i + ".name", pkg.name());
+      data.setValue("docs.packages." + i + ".link", pkg.htmlPage());
+      TagInfo.makeHDF(data, "docs.packages." + i + ".shortDescr", pkg.firstSentenceTags());
+
+      i++;
+    }
+
+    setPageTitle(data, "Package Index");
+
+    TagInfo.makeHDF(data, "root.descr", Converter.convertTags(root.inlineTags(), null));
+
+    ClearPage.write(data, "packages.cs", filename);
+    ClearPage.write(data, "package-list.cs", javadocDir + "package-list");
+
+    Proofread.writePackages(filename, Converter.convertTags(root.inlineTags(), null));
+  }
+
+  public static void writePackage(PackageInfo pkg) {
+    // these this and the description are in the same directory,
+    // so it's okay
+    Data data = makePackageHDF();
+
+    String name = pkg.name();
+
+    data.setValue("package.name", name);
+    data.setValue("package.since", pkg.getSince());
+    data.setValue("package.descr", "...description...");
+    pkg.setFederatedReferences(data, "package");
+
+    makeClassListHDF(data, "package.interfaces", ClassInfo.sortByName(pkg.interfaces()));
+    makeClassListHDF(data, "package.classes", ClassInfo.sortByName(pkg.ordinaryClasses()));
+    makeClassListHDF(data, "package.enums", ClassInfo.sortByName(pkg.enums()));
+    makeClassListHDF(data, "package.exceptions", ClassInfo.sortByName(pkg.exceptions()));
+    makeClassListHDF(data, "package.errors", ClassInfo.sortByName(pkg.errors()));
+    TagInfo.makeHDF(data, "package.shortDescr", pkg.firstSentenceTags());
+    TagInfo.makeHDF(data, "package.descr", pkg.inlineTags());
+
+    String filename = pkg.htmlPage();
+    setPageTitle(data, name);
+    ClearPage.write(data, "package.cs", filename);
+
+    filename = pkg.fullDescriptionHtmlPage();
+    setPageTitle(data, name + " Details");
+    ClearPage.write(data, "package-descr.cs", filename);
+
+    Proofread.writePackage(filename, pkg.inlineTags());
+  }
+
+  public static void writeClassLists() {
+    int i;
+    Data data = makePackageHDF();
+
+    ClassInfo[] classes = PackageInfo.filterHidden(Converter.convertClasses(root.classes()));
+    if (classes.length == 0) {
+      return;
+    }
+
+    Sorter[] sorted = new Sorter[classes.length];
+    for (i = 0; i < sorted.length; i++) {
+      ClassInfo cl = classes[i];
+      String name = cl.name();
+      sorted[i] = new Sorter(name, cl);
+    }
+
+    Arrays.sort(sorted);
+
+    // make a pass and resolve ones that have the same name
+    int firstMatch = 0;
+    String lastName = sorted[0].label;
+    for (i = 1; i < sorted.length; i++) {
+      String s = sorted[i].label;
+      if (!lastName.equals(s)) {
+        if (firstMatch != i - 1) {
+          // there were duplicates
+          for (int j = firstMatch; j < i; j++) {
+            PackageInfo pkg = ((ClassInfo) sorted[j].data).containingPackage();
+            if (pkg != null) {
+              sorted[j].label = sorted[j].label + " (" + pkg.name() + ")";
+            }
+          }
+        }
+        firstMatch = i;
+        lastName = s;
+      }
+    }
+
+    // and sort again
+    Arrays.sort(sorted);
+
+    for (i = 0; i < sorted.length; i++) {
+      String s = sorted[i].label;
+      ClassInfo cl = (ClassInfo) sorted[i].data;
+      char first = Character.toUpperCase(s.charAt(0));
+      cl.makeShortDescrHDF(data, "docs.classes." + first + '.' + i);
+    }
+
+    setPageTitle(data, "Class Index");
+    ClearPage.write(data, "classes.cs", javadocDir + "classes" + htmlExtension);
+  }
+
+  // we use the word keywords because "index" means something else in html land
+  // the user only ever sees the word index
+  /*
+   * public static void writeKeywords() { ArrayList<KeywordEntry> keywords = new
+   * ArrayList<KeywordEntry>();
+   * 
+   * ClassInfo[] classes = PackageInfo.filterHidden(Converter.convertClasses(root.classes()));
+   * 
+   * for (ClassInfo cl: classes) { cl.makeKeywordEntries(keywords); }
+   * 
+   * HDF data = makeHDF();
+   * 
+   * Collections.sort(keywords);
+   * 
+   * int i=0; for (KeywordEntry entry: keywords) { String base = "keywords." + entry.firstChar() +
+   * "." + i; entry.makeHDF(data, base); i++; }
+   * 
+   * setPageTitle(data, "Index"); ClearPage.write(data, "keywords.cs", javadocDir + "keywords" +
+   * htmlExtension); }
+   */
+
+  public static void writeHierarchy() {
+    ClassInfo[] classes = Converter.rootClasses();
+    ArrayList<ClassInfo> info = new ArrayList<ClassInfo>();
+    for (ClassInfo cl : classes) {
+      if (!cl.isHidden()) {
+        info.add(cl);
+      }
+    }
+    Data data = makePackageHDF();
+    Hierarchy.makeHierarchy(data, info.toArray(new ClassInfo[info.size()]));
+    setPageTitle(data, "Class Hierarchy");
+    ClearPage.write(data, "hierarchy.cs", javadocDir + "hierarchy" + htmlExtension);
+  }
+
+  public static void writeClasses() {
+    ClassInfo[] classes = Converter.rootClasses();
+
+    for (ClassInfo cl : classes) {
+      Data data = makePackageHDF();
+      if (!cl.isHidden()) {
+        writeClass(cl, data);
+      }
+    }
+  }
+
+  public static void writeClass(ClassInfo cl, Data data) {
+    cl.makeHDF(data);
+
+    setPageTitle(data, cl.name());
+    ClearPage.write(data, "class.cs", cl.htmlPage());
+
+    Proofread.writeClass(cl.htmlPage(), cl);
+  }
+
+  public static void makeClassListHDF(Data data, String base, ClassInfo[] classes) {
+    for (int i = 0; i < classes.length; i++) {
+      ClassInfo cl = classes[i];
+      if (!cl.isHidden()) {
+        cl.makeShortDescrHDF(data, base + "." + i);
+      }
+    }
+  }
+
+  public static String linkTarget(String source, String target) {
+    String[] src = source.split("/");
+    String[] tgt = target.split("/");
+
+    int srclen = src.length;
+    int tgtlen = tgt.length;
+
+    int same = 0;
+    while (same < (srclen - 1) && same < (tgtlen - 1) && (src[same].equals(tgt[same]))) {
+      same++;
+    }
+
+    String s = "";
+
+    int up = srclen - same - 1;
+    for (int i = 0; i < up; i++) {
+      s += "../";
+    }
+
+
+    int N = tgtlen - 1;
+    for (int i = same; i < N; i++) {
+      s += tgt[i] + '/';
+    }
+    s += tgt[tgtlen - 1];
+
+    return s;
+  }
+
+  /**
+   * Returns true if the given element has an @hide or @pending annotation.
+   */
+  private static boolean hasHideAnnotation(Doc doc) {
+    String comment = doc.getRawCommentText();
+    return comment.indexOf("@hide") != -1 || comment.indexOf("@pending") != -1;
+  }
+
+  /**
+   * Returns true if the given element is hidden.
+   */
+  private static boolean isHidden(Doc doc) {
+    // Methods, fields, constructors.
+    if (doc instanceof MemberDoc) {
+      return hasHideAnnotation(doc);
+    }
+
+    // Classes, interfaces, enums, annotation types.
+    if (doc instanceof ClassDoc) {
+      ClassDoc classDoc = (ClassDoc) doc;
+
+      // Check the containing package.
+      if (hasHideAnnotation(classDoc.containingPackage())) {
+        return true;
+      }
+
+      // Check the class doc and containing class docs if this is a
+      // nested class.
+      ClassDoc current = classDoc;
+      do {
+        if (hasHideAnnotation(current)) {
+          return true;
+        }
+
+        current = current.containingClass();
+      } while (current != null);
+    }
+
+    return false;
+  }
+
+  /**
+   * Filters out hidden elements.
+   */
+  private static Object filterHidden(Object o, Class<?> expected) {
+    if (o == null) {
+      return null;
+    }
+
+    Class type = o.getClass();
+    if (type.getName().startsWith("com.sun.")) {
+      // TODO: Implement interfaces from superclasses, too.
+      return Proxy
+          .newProxyInstance(type.getClassLoader(), type.getInterfaces(), new HideHandler(o));
+    } else if (o instanceof Object[]) {
+      Class<?> componentType = expected.getComponentType();
+      Object[] array = (Object[]) o;
+      List<Object> list = new ArrayList<Object>(array.length);
+      for (Object entry : array) {
+        if ((entry instanceof Doc) && isHidden((Doc) entry)) {
+          continue;
+        }
+        list.add(filterHidden(entry, componentType));
+      }
+      return list.toArray((Object[]) Array.newInstance(componentType, list.size()));
+    } else {
+      return o;
+    }
+  }
+
+  /**
+   * Filters hidden elements out of method return values.
+   */
+  private static class HideHandler implements InvocationHandler {
+
+    private final Object target;
+
+    public HideHandler(Object target) {
+      this.target = target;
+    }
+
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+      String methodName = method.getName();
+      if (args != null) {
+        if (methodName.equals("compareTo") || methodName.equals("equals")
+            || methodName.equals("overrides") || methodName.equals("subclassOf")) {
+          args[0] = unwrap(args[0]);
+        }
+      }
+
+      if (methodName.equals("getRawCommentText")) {
+        return filterComment((String) method.invoke(target, args));
+      }
+
+      // escape "&" in disjunctive types.
+      if (proxy instanceof Type && methodName.equals("toString")) {
+        return ((String) method.invoke(target, args)).replace("&", "&amp;");
+      }
+
+      try {
+        return filterHidden(method.invoke(target, args), method.getReturnType());
+      } catch (InvocationTargetException e) {
+        throw e.getTargetException();
+      }
+    }
+
+    private String filterComment(String s) {
+      if (s == null) {
+        return null;
+      }
+
+      s = s.trim();
+
+      // Work around off by one error
+      while (s.length() >= 5 && s.charAt(s.length() - 5) == '{') {
+        s += "&nbsp;";
+      }
+
+      return s;
+    }
+
+    private static Object unwrap(Object proxy) {
+      if (proxy instanceof Proxy) return ((HideHandler) Proxy.getInvocationHandler(proxy)).target;
+      return proxy;
+    }
+  }
+
+  /**
+   * Collect the values used by the Dev tools and write them in files packaged with the SDK
+   * 
+   * @param output the ouput directory for the files.
+   */
+  private static void writeSdkValues(String output) {
+    ArrayList<String> activityActions = new ArrayList<String>();
+    ArrayList<String> broadcastActions = new ArrayList<String>();
+    ArrayList<String> serviceActions = new ArrayList<String>();
+    ArrayList<String> categories = new ArrayList<String>();
+    ArrayList<String> features = new ArrayList<String>();
+
+    ArrayList<ClassInfo> layouts = new ArrayList<ClassInfo>();
+    ArrayList<ClassInfo> widgets = new ArrayList<ClassInfo>();
+    ArrayList<ClassInfo> layoutParams = new ArrayList<ClassInfo>();
+
+    ClassInfo[] classes = Converter.allClasses();
+
+    // Go through all the fields of all the classes, looking SDK stuff.
+    for (ClassInfo clazz : classes) {
+
+      // first check constant fields for the SdkConstant annotation.
+      FieldInfo[] fields = clazz.allSelfFields();
+      for (FieldInfo field : fields) {
+        Object cValue = field.constantValue();
+        if (cValue != null) {
+          AnnotationInstanceInfo[] annotations = field.annotations();
+          if (annotations.length > 0) {
+            for (AnnotationInstanceInfo annotation : annotations) {
+              if (SDK_CONSTANT_ANNOTATION.equals(annotation.type().qualifiedName())) {
+                AnnotationValueInfo[] values = annotation.elementValues();
+                if (values.length > 0) {
+                  String type = values[0].valueString();
+                  if (SDK_CONSTANT_TYPE_ACTIVITY_ACTION.equals(type)) {
+                    activityActions.add(cValue.toString());
+                  } else if (SDK_CONSTANT_TYPE_BROADCAST_ACTION.equals(type)) {
+                    broadcastActions.add(cValue.toString());
+                  } else if (SDK_CONSTANT_TYPE_SERVICE_ACTION.equals(type)) {
+                    serviceActions.add(cValue.toString());
+                  } else if (SDK_CONSTANT_TYPE_CATEGORY.equals(type)) {
+                    categories.add(cValue.toString());
+                  } else if (SDK_CONSTANT_TYPE_FEATURE.equals(type)) {
+                    features.add(cValue.toString());
+                  }
+                }
+                break;
+              }
+            }
+          }
+        }
+      }
+
+      // Now check the class for @Widget or if its in the android.widget package
+      // (unless the class is hidden or abstract, or non public)
+      if (clazz.isHidden() == false && clazz.isPublic() && clazz.isAbstract() == false) {
+        boolean annotated = false;
+        AnnotationInstanceInfo[] annotations = clazz.annotations();
+        if (annotations.length > 0) {
+          for (AnnotationInstanceInfo annotation : annotations) {
+            if (SDK_WIDGET_ANNOTATION.equals(annotation.type().qualifiedName())) {
+              widgets.add(clazz);
+              annotated = true;
+              break;
+            } else if (SDK_LAYOUT_ANNOTATION.equals(annotation.type().qualifiedName())) {
+              layouts.add(clazz);
+              annotated = true;
+              break;
+            }
+          }
+        }
+
+        if (annotated == false) {
+          // lets check if this is inside android.widget
+          PackageInfo pckg = clazz.containingPackage();
+          String packageName = pckg.name();
+          if ("android.widget".equals(packageName) || "android.view".equals(packageName)) {
+            // now we check what this class inherits either from android.view.ViewGroup
+            // or android.view.View, or android.view.ViewGroup.LayoutParams
+            int type = checkInheritance(clazz);
+            switch (type) {
+              case TYPE_WIDGET:
+                widgets.add(clazz);
+                break;
+              case TYPE_LAYOUT:
+                layouts.add(clazz);
+                break;
+              case TYPE_LAYOUT_PARAM:
+                layoutParams.add(clazz);
+                break;
+            }
+          }
+        }
+      }
+    }
+
+    // now write the files, whether or not the list are empty.
+    // the SDK built requires those files to be present.
+
+    Collections.sort(activityActions);
+    writeValues(output + "/activity_actions.txt", activityActions);
+
+    Collections.sort(broadcastActions);
+    writeValues(output + "/broadcast_actions.txt", broadcastActions);
+
+    Collections.sort(serviceActions);
+    writeValues(output + "/service_actions.txt", serviceActions);
+
+    Collections.sort(categories);
+    writeValues(output + "/categories.txt", categories);
+
+    Collections.sort(features);
+    writeValues(output + "/features.txt", features);
+
+    // before writing the list of classes, we do some checks, to make sure the layout params
+    // are enclosed by a layout class (and not one that has been declared as a widget)
+    for (int i = 0; i < layoutParams.size();) {
+      ClassInfo layoutParamClass = layoutParams.get(i);
+      ClassInfo containingClass = layoutParamClass.containingClass();
+      if (containingClass == null || layouts.indexOf(containingClass) == -1) {
+        layoutParams.remove(i);
+      } else {
+        i++;
+      }
+    }
+
+    writeClasses(output + "/widgets.txt", widgets, layouts, layoutParams);
+  }
+
+  /**
+   * Writes a list of values into a text files.
+   * 
+   * @param pathname the absolute os path of the output file.
+   * @param values the list of values to write.
+   */
+  private static void writeValues(String pathname, ArrayList<String> values) {
+    FileWriter fw = null;
+    BufferedWriter bw = null;
+    try {
+      fw = new FileWriter(pathname, false);
+      bw = new BufferedWriter(fw);
+
+      for (String value : values) {
+        bw.append(value).append('\n');
+      }
+    } catch (IOException e) {
+      // pass for now
+    } finally {
+      try {
+        if (bw != null) bw.close();
+      } catch (IOException e) {
+        // pass for now
+      }
+      try {
+        if (fw != null) fw.close();
+      } catch (IOException e) {
+        // pass for now
+      }
+    }
+  }
+
+  /**
+   * Writes the widget/layout/layout param classes into a text files.
+   * 
+   * @param pathname the absolute os path of the output file.
+   * @param widgets the list of widget classes to write.
+   * @param layouts the list of layout classes to write.
+   * @param layoutParams the list of layout param classes to write.
+   */
+  private static void writeClasses(String pathname, ArrayList<ClassInfo> widgets,
+      ArrayList<ClassInfo> layouts, ArrayList<ClassInfo> layoutParams) {
+    FileWriter fw = null;
+    BufferedWriter bw = null;
+    try {
+      fw = new FileWriter(pathname, false);
+      bw = new BufferedWriter(fw);
+
+      // write the 3 types of classes.
+      for (ClassInfo clazz : widgets) {
+        writeClass(bw, clazz, 'W');
+      }
+      for (ClassInfo clazz : layoutParams) {
+        writeClass(bw, clazz, 'P');
+      }
+      for (ClassInfo clazz : layouts) {
+        writeClass(bw, clazz, 'L');
+      }
+    } catch (IOException e) {
+      // pass for now
+    } finally {
+      try {
+        if (bw != null) bw.close();
+      } catch (IOException e) {
+        // pass for now
+      }
+      try {
+        if (fw != null) fw.close();
+      } catch (IOException e) {
+        // pass for now
+      }
+    }
+  }
+
+  /**
+   * Writes a class name and its super class names into a {@link BufferedWriter}.
+   * 
+   * @param writer the BufferedWriter to write into
+   * @param clazz the class to write
+   * @param prefix the prefix to put at the beginning of the line.
+   * @throws IOException
+   */
+  private static void writeClass(BufferedWriter writer, ClassInfo clazz, char prefix)
+      throws IOException {
+    writer.append(prefix).append(clazz.qualifiedName());
+    ClassInfo superClass = clazz;
+    while ((superClass = superClass.superclass()) != null) {
+      writer.append(' ').append(superClass.qualifiedName());
+    }
+    writer.append('\n');
+  }
+
+  /**
+   * Checks the inheritance of {@link ClassInfo} objects. This method return
+   * <ul>
+   * <li>{@link #TYPE_LAYOUT}: if the class extends <code>android.view.ViewGroup</code></li>
+   * <li>{@link #TYPE_WIDGET}: if the class extends <code>android.view.View</code></li>
+   * <li>{@link #TYPE_LAYOUT_PARAM}: if the class extends
+   * <code>android.view.ViewGroup$LayoutParams</code></li>
+   * <li>{@link #TYPE_NONE}: in all other cases</li>
+   * </ul>
+   * 
+   * @param clazz the {@link ClassInfo} to check.
+   */
+  private static int checkInheritance(ClassInfo clazz) {
+    if ("android.view.ViewGroup".equals(clazz.qualifiedName())) {
+      return TYPE_LAYOUT;
+    } else if ("android.view.View".equals(clazz.qualifiedName())) {
+      return TYPE_WIDGET;
+    } else if ("android.view.ViewGroup.LayoutParams".equals(clazz.qualifiedName())) {
+      return TYPE_LAYOUT_PARAM;
+    }
+
+    ClassInfo parent = clazz.superclass();
+    if (parent != null) {
+      return checkInheritance(parent);
+    }
+
+    return TYPE_NONE;
+  }
+}
diff --git a/src/com/google/doclava/DoclavaDiff.java b/src/com/google/doclava/DoclavaDiff.java
new file mode 100644
index 0000000..e043246
--- /dev/null
+++ b/src/com/google/doclava/DoclavaDiff.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.JSilver;
+import com.google.clearsilver.jsilver.data.Data;
+import com.google.clearsilver.jsilver.resourceloader.CompositeResourceLoader;
+import com.google.clearsilver.jsilver.resourceloader.FileSystemResourceLoader;
+import com.google.clearsilver.jsilver.resourceloader.ResourceLoader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class is used to generate a web page highlighting the differences and
+ * similarities among various Java libraries.
+ *
+ */
+public final class DoclavaDiff {
+  private final String outputDir;
+  private final JSilver jSilver;
+  private final List<FederatedSite> sites = new ArrayList<FederatedSite>();
+  
+  public static void main(String[] args) {
+    new DoclavaDiff(args).generateSite();
+  }
+  
+  public DoclavaDiff(String[] args) {
+    // TODO: options parsing
+    try {
+      sites.add(new FederatedSite("Android", new URL("http://manatee/doclava/android")));
+      sites.add(new FederatedSite("GWT", new URL("http://manatee/doclava/gwt")));
+      //sites.add(new FederatedSite("Crore", new URL("http://manatee/doclava/crore")));
+      outputDir = "build";
+    } catch (Exception e) {
+      throw new AssertionError(e);
+    }
+    
+    // TODO: accept external templates
+    List<ResourceLoader> resourceLoaders = new ArrayList<ResourceLoader>();
+    resourceLoaders.add(new FileSystemResourceLoader("assets/templates"));
+
+    ResourceLoader compositeResourceLoader = new CompositeResourceLoader(resourceLoaders);
+    jSilver = new JSilver(compositeResourceLoader);
+  }
+  
+  public void generateSite() {    
+    Data data = generateHdf();
+    generateHtml("diff.cs", data, new File(outputDir + "/diff.html"));
+  }
+  
+  /**
+   * Creates an HDF with this structure:
+   * <pre>
+   * sites.0.name = Android
+   * sites.0.url = http://developer.android.com/reference
+   * sites.1.name = GWT
+   * sites.1.url = http://gwt.googlecode.com
+   * packages.0.name = java.lang
+   * packages.0.sites.0.hasPackage = 1
+   * packages.0.sites.0.link = http://developer.android.com/reference/java/lang
+   * packages.0.sites.1.hasPackage = 0
+   * packages.0.classes.0.qualifiedName = java.lang.Object
+   * packages.0.classes.0.sites.0.hasClass = 1
+   * packages.0.classes.0.sites.0.link = http://developer.android.com/reference/java/lang/Object
+   * packages.0.classes.0.sites.1.hasClass = 0 
+   * packages.0.classes.0.methods.0.signature = wait()
+   * packages.0.classes.0.methods.0.sites.0.hasMethod = 1
+   * packages.0.classes.0.methods.0.sites.0.link = http://developer.android.com/reference/java/lang/Object#wait
+   * packages.0.classes.0.methods.0.sites.1.hasMethod = 0
+   * </pre>
+   */
+  private Data generateHdf() {
+    Data data = jSilver.createData();
+    
+    data.setValue("triangle.opened", "../assets/templates/assets/images/triangle-opened.png");
+    data.setValue("triangle.closed", "../assets/templates/assets/images/triangle-closed.png");
+    
+    int i = 0;
+    for (FederatedSite site : sites) {
+      String base = "sites." + (i++);
+      data.setValue(base + ".name", site.name());
+      data.setValue(base + ".url", site.baseUrl().toString());
+    }
+    
+    List<String> allPackages = knownPackages(sites);
+    
+    int p = 0;
+    for (String pkg : allPackages) {
+      PackageInfo packageInfo = new PackageInfo(pkg);
+      String packageBase = "packages." + (p++);
+      data.setValue(packageBase + ".name", pkg);
+      
+      int s = 0;
+      for (FederatedSite site : sites) {
+        String siteBase = packageBase + ".sites." + (s++);
+        if (site.apiInfo().getPackages().containsKey(pkg)) {
+          data.setValue(siteBase + ".hasPackage", "1");
+          data.setValue(siteBase + ".link", site.linkFor(packageInfo.htmlPage()));
+        } else {
+          data.setValue(siteBase + ".hasPackage", "0");
+        }
+      }
+      
+      if (packageUniqueToSite(pkg, sites)) {
+        continue;
+      }
+            
+      List<String> packageClasses = knownClassesForPackage(pkg, sites);
+      int c = 0;
+      for (String qualifiedClassName : packageClasses) {
+        String classBase = packageBase + ".classes." + (c++);
+        data.setValue(classBase + ".qualifiedName", qualifiedClassName);
+        
+        s = 0;
+        for (FederatedSite site : sites) {
+          String siteBase = classBase + ".sites." + (s++);
+          ClassInfo classInfo = site.apiInfo().findClass(qualifiedClassName);
+          if (classInfo != null) {
+            data.setValue(siteBase + ".hasClass", "1");
+            data.setValue(siteBase + ".link", site.linkFor(classInfo.htmlPage()));
+          } else {
+            data.setValue(siteBase + ".hasClass", "0");
+          }
+        }
+        
+        if (agreeOnClass(qualifiedClassName, sites)) {
+          continue;
+        }
+        
+        if (classUniqueToSite(qualifiedClassName, sites)) {
+          continue;
+        }
+        
+        int m = 0;
+        List<MethodInfo> methods = knownMethodsForClass(qualifiedClassName, sites);
+        for (MethodInfo method : methods) {
+          if (agreeOnMethod(qualifiedClassName, method, sites)) {
+            continue;
+          }
+          
+          String methodBase = classBase + ".methods." + (m++);
+          data.setValue(methodBase + ".signature", method.prettySignature());
+          int k = 0;
+          for (FederatedSite site : sites) {
+            String siteBase = methodBase + ".sites." + (k++);
+            if (site.apiInfo().findClass(qualifiedClassName) == null) {
+              data.setValue(siteBase + ".hasMethod", "0");
+              continue;
+            }
+            Map<String,MethodInfo> siteMethods
+                = site.apiInfo().findClass(qualifiedClassName).allMethods();
+            if (siteMethods.containsKey(method.getHashableName())) {
+              data.setValue(siteBase + ".hasMethod", "1");
+              data.setValue(siteBase + ".link", site.linkFor(method.htmlPage()));
+            } else {
+              data.setValue(siteBase + ".hasMethod", "0");
+            }
+          }
+        }
+      }
+    }
+    
+    return data;
+  }
+  
+  /**
+   * Returns a list of all known packages from all sites.
+   */
+  private List<String> knownPackages(List<FederatedSite> sites) {
+    Set<String> allPackages = new LinkedHashSet<String>();
+    for (FederatedSite site : sites) {
+      Map<String, PackageInfo> packages = site.apiInfo().getPackages();
+      for (String pkg : packages.keySet()) {
+        allPackages.add(pkg);
+      }
+    }
+    
+    List<String> packages = new ArrayList<String>(allPackages);
+    Collections.sort(packages);
+    return packages;
+  }
+  
+  /**
+   * Returns all known classes from all sites for a given package.
+   */
+  private List<String> knownClassesForPackage(String pkg, List<FederatedSite> sites) {
+    Set<String> allClasses = new LinkedHashSet<String>();
+    for (FederatedSite site : sites) {
+      PackageInfo packageInfo = site.apiInfo().getPackages().get(pkg);
+      if (packageInfo == null) {
+        continue;
+      }
+      HashMap<String, ClassInfo> classes = packageInfo.allClasses();
+      for (Map.Entry<String, ClassInfo> entry : classes.entrySet()) {
+        allClasses.add(entry.getValue().qualifiedName());
+      }
+    }
+    
+    List<String> classes = new ArrayList<String>(allClasses);
+    Collections.sort(classes);
+    return classes;
+  }
+  
+  /**
+   * Returns all known methods from all sites for a given class.
+   */
+  private List<MethodInfo> knownMethodsForClass(String qualifiedClassName,
+      List<FederatedSite> sites) {
+    
+    Map<String, MethodInfo> allMethods = new HashMap<String, MethodInfo>();
+    for (FederatedSite site : sites) {
+      ClassInfo classInfo = site.apiInfo().findClass(qualifiedClassName);
+      if (classInfo == null) {
+        continue;
+      }
+      
+      for (Map.Entry<String, MethodInfo> entry: classInfo.allMethods().entrySet()) {
+        allMethods.put(entry.getKey(), entry.getValue());
+      }
+    }
+    
+    List<MethodInfo> methods = new ArrayList<MethodInfo>();
+    methods.addAll(allMethods.values());
+    return methods;
+  }
+  
+  /**
+   * Returns true if the list of sites all completely agree on the given
+   * package. All sites must possess the package, all classes it contains, and
+   * all methods of each class.
+   */
+  private boolean agreeOnPackage(String pkg, List<FederatedSite> sites) {
+    for (FederatedSite site : sites) {
+      if (site.apiInfo().getPackages().get(pkg) == null) {
+        return false;
+      }
+    }
+    
+    List<String> classes = knownClassesForPackage(pkg, sites);
+    for (String clazz : classes) {
+      if (!agreeOnClass(clazz, sites)) {
+        return false;
+      }
+    }
+    return true;
+  }
+  
+  /**
+   * Returns true if the list of sites all agree on the given class. Each site
+   * must have the class and agree on its methods.
+   */
+  private boolean agreeOnClass(String qualifiedClassName, List<FederatedSite> sites) {
+    List<MethodInfo> methods = knownMethodsForClass(qualifiedClassName, sites);
+    for (MethodInfo method : methods) {
+      if (!agreeOnMethod(qualifiedClassName, method, sites)) {
+        return false;
+      }
+    }
+    return true;
+  }
+  
+  /**
+   * Returns true if the list of sites all contain the given method.
+   */
+  private boolean agreeOnMethod(String qualifiedClassName, MethodInfo method,
+      List<FederatedSite> sites) {
+    
+    for (FederatedSite site : sites) {
+      ClassInfo siteClass = site.apiInfo().findClass(qualifiedClassName);
+      if (siteClass == null) {
+        return false;
+      }
+      
+      if (!siteClass.supportsMethod(method)) {
+        return false;
+      }
+    }
+    return true;
+  }
+  
+  /**
+   * Returns true if the given package is known to exactly one of the given sites.
+   */
+  private boolean packageUniqueToSite(String pkg, List<FederatedSite> sites) {
+    int numSites = 0;
+    for (FederatedSite site : sites) {
+      if (site.apiInfo().getPackages().containsKey(pkg)) {
+        numSites++;
+      }
+    }
+    return numSites == 1;
+  }
+  
+  /**
+   * Returns true if the given class is known to exactly one of the given sites.
+   */
+  private boolean classUniqueToSite(String qualifiedClassName, List<FederatedSite> sites) {
+    int numSites = 0;
+    for (FederatedSite site : sites) {
+      if (site.apiInfo().findClass(qualifiedClassName) != null) {
+        numSites++;
+      }
+    }
+    return numSites == 1;
+  }
+  
+  private void generateHtml(String template, Data data, File file) {
+    ClearPage.ensureDirectory(file);
+    
+    OutputStreamWriter stream = null;
+    try {
+      stream = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
+      String rendered = jSilver.render(template, data);
+      stream.write(rendered, 0, rendered.length());
+    } catch (IOException e) {
+      System.out.println("error: " + e.getMessage() + "; when writing file: " + file.getAbsolutePath());
+    } finally {
+      if (stream != null) {
+        try {
+          stream.close();
+        } catch (IOException ignored) {}
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/com/google/doclava/Errors.java b/src/com/google/doclava/Errors.java
new file mode 100644
index 0000000..6a257fe
--- /dev/null
+++ b/src/com/google/doclava/Errors.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+public class Errors {
+  public static boolean hadError = false;
+  private static boolean warningsAreErrors = false;
+  private static TreeSet<ErrorMessage> allErrors = new TreeSet<ErrorMessage>();
+
+  public static class ErrorMessage implements Comparable {
+    Error error;
+    SourcePositionInfo pos;
+    String msg;
+
+    ErrorMessage(Error e, SourcePositionInfo p, String m) {
+      error = e;
+      pos = p;
+      msg = m;
+    }
+
+    public int compareTo(Object o) {
+      ErrorMessage that = (ErrorMessage) o;
+      int r = this.pos.compareTo(that.pos);
+      if (r != 0) return r;
+      return this.msg.compareTo(that.msg);
+    }
+
+    @Override
+    public String toString() {
+      String whereText = this.pos == null ? "unknown: " : this.pos.toString() + ':';
+      return whereText + this.msg;
+    }
+    
+    public Error error() {
+      return error;
+    }
+  }
+
+  public static void error(Error error, SourcePositionInfo where, String text) {
+    if (error.level == HIDDEN) {
+      return;
+    }
+
+    int level = (!warningsAreErrors && error.level == WARNING) ? WARNING : ERROR;
+    String which = level == WARNING ? " warning " : " error ";
+    String message = which + error.code + ": " + text;
+
+    if (where == null) {
+      where = new SourcePositionInfo("unknown", 0, 0);
+    }
+
+    allErrors.add(new ErrorMessage(error, where, message));
+
+    if (error.level == ERROR || (warningsAreErrors && error.level == WARNING)) {
+      hadError = true;
+    }
+  }
+  
+  public static void clearErrors() {
+    hadError = false;
+    allErrors.clear();
+  }
+
+  public static void printErrors() {
+    printErrors(allErrors);
+  }
+  
+  public static void printErrors(Set<ErrorMessage> errors) {
+    for (ErrorMessage m : errors) {
+      if (m.error.level == WARNING) {
+        System.err.println(m.toString());
+      }
+    }
+    for (ErrorMessage m : errors) {
+      if (m.error.level == ERROR) {
+        System.err.println(m.toString());
+      }
+    }
+  }
+  
+  public static Set<ErrorMessage> getErrors() {
+    return allErrors;
+  }
+
+  public static int HIDDEN = 0;
+  public static int WARNING = 1;
+  public static int ERROR = 2;
+
+  public static void setWarningsAreErrors(boolean val) {
+    warningsAreErrors = val;
+  }
+
+  public static class Error {
+    public int code;
+    public int level;
+
+    public Error(int code, int level) {
+      this.code = code;
+      this.level = level;
+    }
+    
+    public String toString() {
+      return "Error #" + this.code;
+    }
+  }
+
+  public static final Error UNRESOLVED_LINK = new Error(1, WARNING);
+  public static final Error BAD_INCLUDE_TAG = new Error(2, WARNING);
+  public static final Error UNKNOWN_TAG = new Error(3, WARNING);
+  public static final Error UNKNOWN_PARAM_TAG_NAME = new Error(4, WARNING);
+  public static final Error UNDOCUMENTED_PARAMETER = new Error(5, HIDDEN);
+  public static final Error BAD_ATTR_TAG = new Error(6, ERROR);
+  public static final Error BAD_INHERITDOC = new Error(7, HIDDEN);
+  public static final Error HIDDEN_LINK = new Error(8, WARNING);
+  public static final Error HIDDEN_CONSTRUCTOR = new Error(9, WARNING);
+  public static final Error UNAVAILABLE_SYMBOL = new Error(10, ERROR);
+  public static final Error HIDDEN_SUPERCLASS = new Error(11, WARNING);
+  public static final Error DEPRECATED = new Error(12, HIDDEN);
+  public static final Error DEPRECATION_MISMATCH = new Error(13, WARNING);
+  public static final Error MISSING_COMMENT = new Error(14, WARNING);
+  public static final Error IO_ERROR = new Error(15, HIDDEN);
+  public static final Error NO_SINCE_DATA = new Error(16, HIDDEN);
+  public static final Error NO_FEDERATION_DATA = new Error(17, WARNING);
+
+  public static final Error PARSE_ERROR = new Error(18, ERROR); // Apicheck error code 1
+  public static final Error ADDED_PACKAGE = new Error(19, WARNING); // Apicheck error code 2
+  public static final Error ADDED_CLASS = new Error(20, WARNING); // Apicheck error code 3
+  public static final Error ADDED_METHOD = new Error(21, WARNING); // Apicheck error code 4
+  public static final Error ADDED_FIELD = new Error(22, WARNING); // Apicheck error code 5
+  public static final Error ADDED_INTERFACE = new Error(23, WARNING); // Apicheck error code 6
+  public static final Error REMOVED_PACKAGE = new Error(24, WARNING); // Apicheck error code 7
+  public static final Error REMOVED_CLASS = new Error(25, WARNING); // Apicheck error code 8
+  public static final Error REMOVED_METHOD = new Error(26, WARNING); // Apicheck error code 9
+  public static final Error REMOVED_FIELD = new Error(27, WARNING); // Apicheck error code 10
+  public static final Error REMOVED_INTERFACE = new Error(28, WARNING); // Apicheck error code 11
+  public static final Error CHANGED_STATIC = new Error(29, WARNING); // Apicheck error code 12
+  public static final Error CHANGED_FINAL = new Error(30, WARNING); // Apicheck error code 13
+  public static final Error CHANGED_TRANSIENT = new Error(31, WARNING); // Apicheck error code 14
+  public static final Error CHANGED_VOLATILE = new Error(32, WARNING); // Apicheck error code 15
+  public static final Error CHANGED_TYPE = new Error(33, WARNING); // Apicheck error code 16
+  public static final Error CHANGED_VALUE = new Error(34, WARNING); // Apicheck error code 17
+  public static final Error CHANGED_SUPERCLASS = new Error(35, WARNING); // Apicheck error code 18
+  public static final Error CHANGED_SCOPE = new Error(36, WARNING); // Apicheck error code 19
+  public static final Error CHANGED_ABSTRACT = new Error(37, WARNING); // Apicheck error code 20
+  public static final Error CHANGED_THROWS = new Error(38, WARNING); // Apicheck error code 21
+  public static final Error CHANGED_NATIVE = new Error(39, HIDDEN); // Apicheck error code 22
+  public static final Error CHANGED_CLASS = new Error(40, WARNING); // Apicheck error code 23
+  public static final Error CHANGED_DEPRECATED = new Error(41, WARNING); // Apicheck error code 24
+  public static final Error CHANGED_SYNCHRONIZED = new Error(42, ERROR); // Apicheck error code 25
+  
+  public static final Error[] ERRORS =
+      {UNRESOLVED_LINK, BAD_INCLUDE_TAG, UNKNOWN_TAG, UNKNOWN_PARAM_TAG_NAME,
+          UNDOCUMENTED_PARAMETER, BAD_ATTR_TAG, BAD_INHERITDOC, HIDDEN_LINK, HIDDEN_CONSTRUCTOR,
+          UNAVAILABLE_SYMBOL, HIDDEN_SUPERCLASS, DEPRECATED, IO_ERROR, NO_SINCE_DATA,
+          NO_FEDERATION_DATA, PARSE_ERROR, ADDED_PACKAGE, ADDED_CLASS, ADDED_METHOD, ADDED_FIELD,
+          ADDED_INTERFACE, REMOVED_PACKAGE, REMOVED_CLASS, REMOVED_METHOD, REMOVED_FIELD,
+          REMOVED_INTERFACE, CHANGED_STATIC, CHANGED_FINAL, CHANGED_TRANSIENT, CHANGED_VOLATILE,
+          CHANGED_TYPE, CHANGED_VALUE, CHANGED_SUPERCLASS, CHANGED_SCOPE, CHANGED_ABSTRACT,
+          CHANGED_THROWS, CHANGED_NATIVE, CHANGED_CLASS, CHANGED_DEPRECATED, CHANGED_SYNCHRONIZED};
+
+  public static boolean setErrorLevel(int code, int level) {
+    for (Error e : ERRORS) {
+      if (e.code == code) {
+        e.level = level;
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/com/google/doclava/FederatedSite.java b/src/com/google/doclava/FederatedSite.java
new file mode 100644
index 0000000..2aa12cf
--- /dev/null
+++ b/src/com/google/doclava/FederatedSite.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.doclava.apicheck.ApiCheck;
+import com.google.doclava.apicheck.ApiInfo;
+import com.google.doclava.apicheck.ApiParseException;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * A remote source of documentation that can be linked against. A federated
+ * site represents a library that has packages, classes, and members that may
+ * be referenced or shared across codebases.
+ */
+public final class FederatedSite {
+  private final String name;
+  private final URL baseUrl;
+  private final ApiInfo apiInfo;
+  
+  public FederatedSite(String name, URL baseUrl) throws ApiParseException {
+    this.name = name;
+    this.baseUrl = baseUrl;
+    
+    try {
+      URL xmlUrl = new URL(baseUrl + "/xml/current.xml");
+      this.apiInfo = new ApiCheck().parseApi(xmlUrl);
+    } catch (MalformedURLException e) {
+      throw new AssertionError(e);
+    }
+  }
+  
+  public String linkFor(String htmlPage) {
+    return baseUrl + "/" + htmlPage;
+  }
+
+  public String name() {
+    return name;
+  }
+
+  public ApiInfo apiInfo() {
+    return apiInfo;
+  }
+  
+  public URL baseUrl() {
+    return baseUrl;
+  }
+}
\ No newline at end of file
diff --git a/src/com/google/doclava/FederationTagger.java b/src/com/google/doclava/FederationTagger.java
new file mode 100644
index 0000000..2b54d9e
--- /dev/null
+++ b/src/com/google/doclava/FederationTagger.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.doclava.apicheck.ApiCheck;
+import com.google.doclava.apicheck.ApiInfo;
+import com.google.doclava.apicheck.ApiParseException;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Cross-references documentation among different libraries. A FederationTagger
+ * is populated with a list of {@link FederatedSite} objects which are linked
+ * against when overlapping content is discovered.
+ */
+public final class FederationTagger {
+  private final List<FederatedSite> federatedSites = new ArrayList<FederatedSite>();
+  
+  /**
+   * Adds a Doclava documentation site for federation. Accepts the base URL of
+   * the remote API.
+   */
+  public void addSite(String name, URL site) {
+    try {
+      federatedSites.add(new FederatedSite(name, site));
+    } catch (ApiParseException e) {
+      String error = "Could not add site for federation: " + site;
+      if (e.getMessage() != null) {
+        error += ": " + e.getMessage();
+      }
+      Errors.error(Errors.NO_FEDERATION_DATA, null, error);
+    }
+  }
+  
+  public void tagAll(ClassInfo[] classDocs) {
+    for (FederatedSite site : federatedSites) {
+      applyFederation(site, classDocs);
+    }
+  }
+  
+  private void applyFederation(FederatedSite federationSource, ClassInfo[] classDocs) {
+    for (ClassInfo classDoc : classDocs) {
+      PackageInfo packageSpec
+          = federationSource.apiInfo().getPackages().get(classDoc.containingPackage().name());
+
+      if (packageSpec == null) {
+        continue;
+      }
+
+      ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name());
+      
+      if (classSpec == null) {
+        continue;
+      }
+      
+      federateMethods(federationSource, classSpec, classDoc);
+      federateConstructors(federationSource, classSpec, classDoc);
+      federateFields(federationSource, classSpec, classDoc);
+      federateClass(federationSource, classDoc);
+      federatePackage(federationSource, classDoc.containingPackage());
+    }
+  }
+
+  private void federateMethods(FederatedSite site, ClassInfo federatedClass, ClassInfo localClass) {
+    for (MethodInfo method : localClass.methods()) {
+      for (ClassInfo superclass : federatedClass.hierarchy()) {
+        if (superclass.allMethods().containsKey(method.getHashableName())) {
+          method.addFederatedReference(site);
+          break;
+        }
+      }
+    }
+  }
+  
+  private void federateConstructors(FederatedSite site, ClassInfo federatedClass,
+      ClassInfo localClass) {
+    for (MethodInfo constructor : localClass.constructors()) {
+      if (federatedClass.hasConstructor(constructor)) {
+        constructor.addFederatedReference(site);
+      }
+    }
+  }
+  
+  private void federateFields(FederatedSite site, ClassInfo federatedClass, ClassInfo localClass) {
+    for (FieldInfo field : localClass.fields()) {
+      if (federatedClass.allFields().containsKey(field.name())) {
+        field.addFederatedReference(site);
+      }
+    }
+  }
+  
+  private void federateClass(FederatedSite source, ClassInfo doc) {
+    doc.addFederatedReference(source);
+  }
+  
+  private void federatePackage(FederatedSite source, PackageInfo pkg) {
+    pkg.addFederatedReference(source);
+  }
+}
\ No newline at end of file
diff --git a/src/com/google/doclava/FieldInfo.java b/src/com/google/doclava/FieldInfo.java
new file mode 100644
index 0000000..112b6cf
--- /dev/null
+++ b/src/com/google/doclava/FieldInfo.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+import java.util.Comparator;
+
+public class FieldInfo extends MemberInfo {
+  public static final Comparator<FieldInfo> comparator = new Comparator<FieldInfo>() {
+    public int compare(FieldInfo a, FieldInfo b) {
+      return a.name().compareTo(b.name());
+    }
+  };
+
+  public FieldInfo(String name, ClassInfo containingClass, ClassInfo realContainingClass,
+      boolean isPublic, boolean isProtected, boolean isPackagePrivate, boolean isPrivate,
+      boolean isFinal, boolean isStatic, boolean isTransient, boolean isVolatile,
+      boolean isSynthetic, TypeInfo type, String rawCommentText, Object constantValue,
+      SourcePositionInfo position, AnnotationInstanceInfo[] annotations) {
+    super(rawCommentText, name, null, containingClass, realContainingClass, isPublic, isProtected,
+        isPackagePrivate, isPrivate, isFinal, isStatic, isSynthetic, chooseKind(isFinal, isStatic),
+        position, annotations);
+    mIsTransient = isTransient;
+    mIsVolatile = isVolatile;
+    mType = type;
+    mConstantValue = constantValue;
+  }
+
+  public FieldInfo cloneForClass(ClassInfo newContainingClass) {
+    return new FieldInfo(name(), newContainingClass, realContainingClass(), isPublic(),
+        isProtected(), isPackagePrivate(), isPrivate(), isFinal(), isStatic(), isTransient(),
+        isVolatile(), isSynthetic(), mType, getRawCommentText(), mConstantValue, position(),
+        annotations());
+  }
+
+  static String chooseKind(boolean isFinal, boolean isStatic) {
+    if (isStatic && isFinal) {
+      return "constant";
+    } else {
+      return "field";
+    }
+  }
+  
+  public String qualifiedName() {
+    String parentQName
+        = (containingClass() != null) ? (containingClass().qualifiedName() + ".") : "";
+    return parentQName + name();
+  }
+
+  public TypeInfo type() {
+    return mType;
+  }
+
+  public boolean isConstant() {
+    return isStatic() && isFinal();
+  }
+
+  public TagInfo[] firstSentenceTags() {
+    return comment().briefTags();
+  }
+
+  public TagInfo[] inlineTags() {
+    return comment().tags();
+  }
+
+  public Object constantValue() {
+    return mConstantValue;
+  }
+
+  public String constantLiteralValue() {
+    return constantLiteralValue(mConstantValue);
+  }
+
+  public void setDeprecated(boolean deprecated) {
+    mDeprecatedKnown = true;
+    mIsDeprecated = deprecated;
+  }
+  
+  public boolean isDeprecated() {
+    if (!mDeprecatedKnown) {
+      boolean commentDeprecated = comment().isDeprecated();
+      boolean annotationDeprecated = false;
+      for (AnnotationInstanceInfo annotation : annotations()) {
+        if (annotation.type().qualifiedName().equals("java.lang.Deprecated")) {
+          annotationDeprecated = true;
+          break;
+        }
+      }
+
+      if (commentDeprecated != annotationDeprecated) {
+        Errors.error(Errors.DEPRECATION_MISMATCH, position(), "Field "
+            + mContainingClass.qualifiedName() + "." + name()
+            + ": @Deprecated annotation and @deprecated comment do not match");
+      }
+
+      mIsDeprecated = commentDeprecated | annotationDeprecated;
+      mDeprecatedKnown = true;
+    }
+    return mIsDeprecated;
+  }
+
+  public static String constantLiteralValue(Object val) {
+    String str = null;
+    if (val != null) {
+      if (val instanceof Boolean || val instanceof Byte || val instanceof Short
+          || val instanceof Integer) {
+        str = val.toString();
+      }
+      // catch all special values
+      else if (val instanceof Double) {
+        Double dbl = (Double) val;
+        if (dbl.toString().equals("Infinity")) {
+          str = "(1.0 / 0.0)";
+        } else if (dbl.toString().equals("-Infinity")) {
+          str = "(-1.0 / 0.0)";
+        } else if (dbl.isNaN()) {
+          str = "(0.0 / 0.0)";
+        } else {
+          str = dbl.toString();
+        }
+      } else if (val instanceof Long) {
+        str = val.toString() + "L";
+      } else if (val instanceof Float) {
+        Float fl = (Float) val;
+        if (fl.toString().equals("Infinity")) {
+          str = "(1.0f / 0.0f)";
+        } else if (fl.toString().equals("-Infinity")) {
+          str = "(-1.0f / 0.0f)";
+        } else if (fl.isNaN()) {
+          str = "(0.0f / 0.0f)";
+        } else {
+          str = val.toString() + "f";
+        }
+      } else if (val instanceof Character) {
+        str = String.format("\'\\u%04x\'", val);
+      } else if (val instanceof String) {
+        str = "\"" + javaEscapeString((String) val) + "\"";
+      } else {
+        str = "<<<<" + val.toString() + ">>>>";
+      }
+    }
+    if (str == null) {
+      str = "null";
+    }
+    return str;
+  }
+
+  public static String javaEscapeString(String str) {
+    String result = "";
+    final int N = str.length();
+    for (int i = 0; i < N; i++) {
+      char c = str.charAt(i);
+      if (c == '\\') {
+        result += "\\\\";
+      } else if (c == '\t') {
+        result += "\\t";
+      } else if (c == '\b') {
+        result += "\\b";
+      } else if (c == '\r') {
+        result += "\\r";
+      } else if (c == '\n') {
+        result += "\\n";
+      } else if (c == '\f') {
+        result += "\\f";
+      } else if (c == '\'') {
+        result += "\\'";
+      } else if (c == '\"') {
+        result += "\\\"";
+      } else if (c >= ' ' && c <= '~') {
+        result += c;
+      } else {
+        result += String.format("\\u%04x", new Integer((int) c));
+      }
+    }
+    return result;
+  }
+
+
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".kind", kind());
+    type().makeHDF(data, base + ".type");
+    data.setValue(base + ".name", name());
+    data.setValue(base + ".href", htmlPage());
+    data.setValue(base + ".anchor", anchor());
+    TagInfo.makeHDF(data, base + ".shortDescr", firstSentenceTags());
+    TagInfo.makeHDF(data, base + ".descr", inlineTags());
+    TagInfo.makeHDF(data, base + ".deprecated", comment().deprecatedTags());
+    TagInfo.makeHDF(data, base + ".seeAlso", comment().seeTags());
+    data.setValue(base + ".since", getSince());
+    data.setValue(base + ".final", isFinal() ? "final" : "");
+    data.setValue(base + ".static", isStatic() ? "static" : "");
+    if (isPublic()) {
+      data.setValue(base + ".scope", "public");
+    } else if (isProtected()) {
+      data.setValue(base + ".scope", "protected");
+    } else if (isPackagePrivate()) {
+      data.setValue(base + ".scope", "");
+    } else if (isPrivate()) {
+      data.setValue(base + ".scope", "private");
+    }
+    Object val = mConstantValue;
+    if (val != null) {
+      String dec = null;
+      String hex = null;
+      String str = null;
+
+      if (val instanceof Boolean) {
+        str = ((Boolean) val).toString();
+      } else if (val instanceof Byte) {
+        dec = String.format("%d", val);
+        hex = String.format("0x%02x", val);
+      } else if (val instanceof Character) {
+        dec = String.format("\'%c\'", val);
+        hex = String.format("0x%04x", val);
+      } else if (val instanceof Double) {
+        str = ((Double) val).toString();
+      } else if (val instanceof Float) {
+        str = ((Float) val).toString();
+      } else if (val instanceof Integer) {
+        dec = String.format("%d", val);
+        hex = String.format("0x%08x", val);
+      } else if (val instanceof Long) {
+        dec = String.format("%d", val);
+        hex = String.format("0x%016x", val);
+      } else if (val instanceof Short) {
+        dec = String.format("%d", val);
+        hex = String.format("0x%04x", val);
+      } else if (val instanceof String) {
+        str = "\"" + ((String) val) + "\"";
+      } else {
+        str = "";
+      }
+
+      if (dec != null && hex != null) {
+        data.setValue(base + ".constantValue.dec", Doclava.escape(dec));
+        data.setValue(base + ".constantValue.hex", Doclava.escape(hex));
+      } else {
+        data.setValue(base + ".constantValue.str", Doclava.escape(str));
+        data.setValue(base + ".constantValue.isString", "1");
+      }
+    }
+    
+    setFederatedReferences(data, base);
+  }
+
+  @Override
+  public boolean isExecutable() {
+    return false;
+  }
+
+  public boolean isTransient() {
+    return mIsTransient;
+  }
+
+  public boolean isVolatile() {
+    return mIsVolatile;
+  }
+  
+  // Check the declared value with a typed comparison, not a string comparison,
+  // to accommodate toolchains with different fp -> string conversions.
+  private boolean valueEquals(FieldInfo other) {
+    if ((mConstantValue == null) != (other.mConstantValue == null)) {
+      return false;
+    }
+    
+    // Null values are considered equal
+    if (mConstantValue == null) {
+      return true;
+    }
+    
+    // TODO: This method is called through from an XML comparison only right now,
+    // and mConstantValue is always a String. Get rid of this assertion.
+    if (!(mConstantValue instanceof String && other.mConstantValue instanceof String)) {
+      throw new AssertionError("Bad type for field value");
+    }
+    
+    String mValue = (String)mConstantValue;
+    String oValue = (String)other.mConstantValue;
+    // Type mismatch means nonequal
+    if (!mType.equals(other.mType)) {
+      return false;
+    }
+
+    // Floating point gets an implementation-type comparison; all others just use the string
+    // If float/double parse fails, fall back to string comparison -- it means that it's a
+    // canonical droiddoc-generated constant expression that represents a NaN.
+    try {
+      if (mType.equals("float")) {
+        float val = Float.parseFloat(mValue);
+        float otherVal = Float.parseFloat(oValue);
+        return (val == otherVal);
+      } else if (mType.equals("double")) {
+        double val = Double.parseDouble(mValue);
+        double otherVal = Double.parseDouble(oValue);
+        return (val == otherVal);
+      }
+    } catch (NumberFormatException e) {
+      // fall through
+    }
+
+    return mValue.equals(oValue);
+  }
+  
+  public boolean isConsistent(FieldInfo fInfo) {
+    boolean consistent = true;
+    if (!mType.equals(fInfo.mType)) {
+      Errors.error(Errors.CHANGED_TYPE, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed type");
+      consistent = false;
+    } else if (!this.valueEquals(fInfo)) {
+      Errors.error(Errors.CHANGED_VALUE, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed value from " + mConstantValue + " to " + fInfo.mConstantValue);
+      consistent = false;
+    }
+
+    if (!scope().equals(fInfo.scope())) {
+      Errors.error(Errors.CHANGED_SCOPE, fInfo.position(), "Method " + fInfo.qualifiedName()
+          + " changed scope from " + this.scope() + " to " + fInfo.scope());
+      consistent = false;
+    }
+
+    if (mIsStatic != fInfo.mIsStatic) {
+      Errors.error(Errors.CHANGED_STATIC, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed 'static' qualifier");
+      consistent = false;
+    }
+
+    if (mIsFinal != fInfo.mIsFinal) {
+      Errors.error(Errors.CHANGED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed 'final' qualifier");
+      consistent = false;
+    }
+
+    if (mIsTransient != fInfo.mIsTransient) {
+      Errors.error(Errors.CHANGED_TRANSIENT, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed 'transient' qualifier");
+      consistent = false;
+    }
+
+    if (mIsVolatile != fInfo.mIsVolatile) {
+      Errors.error(Errors.CHANGED_VOLATILE, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed 'volatile' qualifier");
+      consistent = false;
+    }
+
+    if (isDeprecated() != fInfo.isDeprecated()) {
+      Errors.error(Errors.CHANGED_DEPRECATED, fInfo.position(), "Field " + fInfo.qualifiedName()
+          + " has changed deprecation state");
+      consistent = false;
+    }
+
+    return consistent;
+  }
+
+  boolean mIsTransient;
+  boolean mIsVolatile;
+  boolean mDeprecatedKnown;
+  boolean mIsDeprecated;
+  TypeInfo mType;
+  Object mConstantValue;
+}
diff --git a/src/com/google/doclava/Hierarchy.java b/src/com/google/doclava/Hierarchy.java
new file mode 100755
index 0000000..0887b63
--- /dev/null
+++ b/src/com/google/doclava/Hierarchy.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.HashMap;
+import java.util.TreeSet;
+import java.util.Set;
+
+public class Hierarchy {
+  public static void makeHierarchy(Data hdf, ClassInfo[] classes) {
+    HashMap<String, TreeSet<String>> nodes = new HashMap<String, TreeSet<String>>();
+
+    for (ClassInfo cl : classes) {
+      String name = cl.qualifiedName();
+
+      TreeSet<String> me = nodes.get(name);
+      if (me == null) {
+        me = new TreeSet<String>();
+        nodes.put(name, me);
+      }
+
+      ClassInfo superclass = cl.superclass();
+      String sname = superclass != null ? superclass.qualifiedName() : null;
+      if (sname != null) {
+        TreeSet<String> s = nodes.get(sname);
+        if (s == null) {
+          s = new TreeSet<String>();
+          nodes.put(sname, s);
+        }
+        s.add(name);
+      }
+    }
+
+    /*
+     * Set<String> keys = nodes.keySet(); for (String n: keys) { System.out.println("class: " + n);
+     * 
+     * TreeSet<String> values = nodes.get(n); for (String v: values) {
+     * System.out.println("       - " + v); } }
+     */
+
+    int depth = depth(nodes, "java.lang.Object");
+
+    hdf.setValue("classes.0", "");
+    hdf.setValue("colspan", "" + depth);
+
+    recurse(nodes, "java.lang.Object", hdf.getChild("classes.0"), depth, depth);
+
+    if (false) {
+      Set<String> keys = nodes.keySet();
+      if (keys.size() > 0) {
+        System.err.println("The following classes are hidden but"
+            + " are superclasses of not-hidden classes");
+        for (String n : keys) {
+          System.err.println("  " + n);
+        }
+      }
+    }
+  }
+
+  private static int depth(HashMap<String, TreeSet<String>> nodes, String name) {
+    int d = 0;
+    TreeSet<String> derived = nodes.get(name);
+    if (derived != null && derived.size() > 0) {
+      for (String s : derived) {
+        int n = depth(nodes, s);
+        if (n > d) {
+          d = n;
+        }
+      }
+    }
+    return d + 1;
+  }
+
+  private static boolean exists(ClassInfo cl) {
+    return cl != null && !cl.isHidden() && cl.isIncluded();
+  }
+
+  private static void recurse(HashMap<String, TreeSet<String>> nodes, String name, Data hdf,
+      int totalDepth, int remainingDepth) {
+    int i;
+
+    hdf.setValue("indent", "" + (totalDepth - remainingDepth - 1));
+    hdf.setValue("colspan", "" + remainingDepth);
+
+    ClassInfo cl = Converter.obtainClass(name);
+
+    hdf.setValue("class.label", cl.name());
+    hdf.setValue("class.qualified", cl.qualifiedName());
+    if (cl.checkLevel()) {
+      hdf.setValue("class.link", cl.htmlPage());
+    }
+
+    if (exists(cl)) {
+      hdf.setValue("exists", "1");
+    }
+
+    i = 0;
+    for (ClassInfo iface : cl.interfaces()) {
+      hdf.setValue("interfaces." + i + ".class.label", iface.name());
+      hdf.setValue("interfaces." + i + ".class.qualified", iface.qualifiedName());
+      if (iface.checkLevel()) {
+        hdf.setValue("interfaces." + i + ".class.link", iface.htmlPage());
+      }
+      if (exists(cl)) {
+        hdf.setValue("interfaces." + i + ".exists", "1");
+      }
+      i++;
+    }
+
+    TreeSet<String> derived = nodes.get(name);
+    if (derived != null && derived.size() > 0) {
+      hdf.setValue("derived", "");
+      Data children = hdf.getChild("derived");
+      i = 0;
+      remainingDepth--;
+      for (String s : derived) {
+        String index = "" + i;
+        children.setValue(index, "");
+        recurse(nodes, s, children.getChild(index), totalDepth, remainingDepth);
+        i++;
+      }
+    }
+
+    nodes.remove(name);
+  }
+}
diff --git a/src/com/google/doclava/InheritedTags.java b/src/com/google/doclava/InheritedTags.java
new file mode 100644
index 0000000..1915721
--- /dev/null
+++ b/src/com/google/doclava/InheritedTags.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public interface InheritedTags {
+  TagInfo[] tags();
+
+  InheritedTags inherited();
+}
diff --git a/src/com/google/doclava/JarUtils.java b/src/com/google/doclava/JarUtils.java
new file mode 100644
index 0000000..a5925ec
--- /dev/null
+++ b/src/com/google/doclava/JarUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+public class JarUtils {
+  /**
+   * Returns the jar file used to load class clazz, or defaultJar if clazz was not loaded from a
+   * jar.
+   */
+  public static JarFile jarForClass(Class<?> clazz, JarFile defaultJar) {
+    String path = "/" + clazz.getName().replace('.', '/') + ".class";
+    URL jarUrl = clazz.getResource(path);
+    if (jarUrl == null) {
+      return defaultJar;
+    }
+
+    String url = jarUrl.toString();
+    int bang = url.indexOf("!");
+    String JAR_URI_PREFIX = "jar:file:";
+    if (url.startsWith(JAR_URI_PREFIX) && bang != -1) {
+      try {
+        return new JarFile(url.substring(JAR_URI_PREFIX.length(), bang));
+      } catch (IOException e) {
+        throw new IllegalStateException("Error loading jar file.", e);
+      }
+    } else {
+      return defaultJar;
+    }
+  }
+
+  /**
+   * Copies a directory from a jar file to an external directory.
+   */
+  public static void copyResourcesToDirectory(JarFile fromJar, String jarDir, String destDir)
+      throws IOException {
+    for (Enumeration<JarEntry> entries = fromJar.entries(); entries.hasMoreElements();) {
+      JarEntry entry = entries.nextElement();
+      if (entry.getName().startsWith(jarDir + "/") && !entry.isDirectory()) {
+        File dest = new File(destDir + "/" + entry.getName().substring(jarDir.length() + 1));
+        File parent = dest.getParentFile();
+        if (parent != null) {
+          parent.mkdirs();
+        }
+
+        FileOutputStream out = new FileOutputStream(dest);
+        InputStream in = fromJar.getInputStream(entry);
+
+        try {
+          byte[] buffer = new byte[8 * 1024];
+
+          int s = 0;
+          while ((s = in.read(buffer)) > 0) {
+            out.write(buffer, 0, s);
+          }
+        } catch (IOException e) {
+          throw new IOException("Could not copy asset from jar file", e);
+        } finally {
+          try {
+            in.close();
+          } catch (IOException ignored) {}
+          try {
+            out.close();
+          } catch (IOException ignored) {}
+        }
+      }
+    }
+
+  }
+
+  private JarUtils() {} // non-instantiable
+}
diff --git a/src/com/google/doclava/KeywordEntry.java b/src/com/google/doclava/KeywordEntry.java
new file mode 100644
index 0000000..28f2d47
--- /dev/null
+++ b/src/com/google/doclava/KeywordEntry.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+class KeywordEntry implements Comparable {
+  KeywordEntry(String label, String href, String comment) {
+    this.label = label;
+    this.href = href;
+    this.comment = comment;
+  }
+
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".label", this.label);
+    data.setValue(base + ".href", this.href);
+    data.setValue(base + ".comment", this.comment);
+  }
+
+  public char firstChar() {
+    return Character.toUpperCase(this.label.charAt(0));
+  }
+
+  public int compareTo(Object that) {
+    return this.label.compareToIgnoreCase(((KeywordEntry) that).label);
+  }
+
+  private String label;
+  private String href;
+  private String comment;
+}
diff --git a/src/com/google/doclava/LinkReference.java b/src/com/google/doclava/LinkReference.java
new file mode 100644
index 0000000..8afba0b
--- /dev/null
+++ b/src/com/google/doclava/LinkReference.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.util.ArrayList;
+
+/**
+ * Class that represents what you see in an link or see tag. This is factored out of SeeTagInfo so
+ * it can be used elsewhere (like AttrTagInfo).
+ */
+public class LinkReference {
+
+  /** The original text. */
+  public String text;
+
+  /** The kind of this tag, if we have a new suggestion after parsing. */
+  public String kind;
+
+  /** The user visible text. */
+  public String label;
+
+  /** The link. */
+  public String href;
+
+  /** The {@link PackageInfo} if any. */
+  public PackageInfo packageInfo;
+
+  /** The {@link ClassInfo} if any. */
+  public ClassInfo classInfo;
+
+  /** The {@link MemberInfo} if any. */
+  public MemberInfo memberInfo;
+
+  /** The name of the referenced member PackageInfo} if any. */
+  public String referencedMemberName;
+
+  /** Set to true if everything is a-ok */
+  public boolean good;
+
+  /**
+   * regex pattern to use when matching explicit "<a href" reference text
+   */
+  private static final Pattern HREF_PATTERN =
+      Pattern.compile("^<a href=\"([^\"]*)\">([^<]*)</a>[ \n\r\t]*$", Pattern.CASE_INSENSITIVE);
+
+  /**
+   * regex pattern to use when matching double-quoted reference text
+   */
+  private static final Pattern QUOTE_PATTERN = Pattern.compile("^\"([^\"]*)\"[ \n\r\t]*$");
+
+  /**
+   * Parse and resolve a link string.
+   * 
+   * @param text the original text
+   * @param base the class or whatever that this link is on
+   * @param pos the original position in the source document
+   * @return a new link reference. It always returns something. If there was an error, it logs it
+   *         and fills in href and label with error text.
+   */
+  public static LinkReference parse(String text, ContainerInfo base, SourcePositionInfo pos,
+      boolean printOnErrors) {
+    LinkReference result = new LinkReference();
+    result.text = text;
+
+    int index;
+    int len = text.length();
+    int pairs = 0;
+    int pound = -1;
+    // split the string
+    done: {
+      for (index = 0; index < len; index++) {
+        char c = text.charAt(index);
+        switch (c) {
+          case '(':
+            pairs++;
+            break;
+          case '[':
+            pairs++;
+            break;
+          case ')':
+            pairs--;
+            break;
+          case ']':
+            pairs--;
+            break;
+          case ' ':
+          case '\t':
+          case '\r':
+          case '\n':
+            if (pairs == 0) {
+              break done;
+            }
+            break;
+          case '#':
+            if (pound < 0) {
+              pound = index;
+            }
+            break;
+        }
+      }
+    }
+    if (index == len && pairs != 0) {
+      Errors.error(Errors.UNRESOLVED_LINK, pos, "unable to parse link/see tag: " + text.trim());
+      return result;
+    }
+
+    int linkend = index;
+
+    for (; index < len; index++) {
+      char c = text.charAt(index);
+      if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
+        break;
+      }
+    }
+
+    result.label = text.substring(index);
+
+    String ref;
+    String mem;
+    if (pound == 0) {
+      ref = null;
+      mem = text.substring(1, linkend);
+    } else if (pound > 0) {
+      ref = text.substring(0, pound);
+      mem = text.substring(pound + 1, linkend);
+    } else {
+      ref = text.substring(0, linkend);
+      mem = null;
+    }
+
+    // parse parameters, if any
+    String[] params = null;
+    String[] paramDimensions = null;
+    boolean varargs = false;
+    if (mem != null) {
+      index = mem.indexOf('(');
+      if (index > 0) {
+        ArrayList<String> paramList = new ArrayList<String>();
+        ArrayList<String> paramDimensionList = new ArrayList<String>();
+        len = mem.length();
+        int start = index + 1;
+        final int START = 0;
+        final int TYPE = 1;
+        final int NAME = 2;
+        int dimension = 0;
+        int arraypair = 0;
+        int state = START;
+        int typestart = 0;
+        int typeend = -1;
+        for (int i = start; i < len; i++) {
+          char c = mem.charAt(i);
+          switch (state) {
+            case START:
+              if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
+                state = TYPE;
+                typestart = i;
+              }
+              break;
+            case TYPE:
+              if (c == '.') {
+                if (mem.length() > i+2 && mem.charAt(i+1) == '.' && mem.charAt(i+2) == '.') {
+                  if (typeend < 0) {
+                    typeend = i;
+                  }
+                  varargs = true;
+                }
+              } else if (c == '[') {
+                if (typeend < 0) {
+                  typeend = i;
+                }
+                dimension++;
+                arraypair++;
+              } else if (c == ']') {
+                arraypair--;
+              } else if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
+                if (typeend < 0) {
+                  typeend = i;
+                }
+              } else {
+                if (typeend >= 0 || c == ')' || c == ',') {
+                  if (typeend < 0) {
+                    typeend = i;
+                  }
+                  String s = mem.substring(typestart, typeend);
+                  paramList.add(s);
+                  s = "";
+                  for (int j = 0; j < dimension; j++) {
+                    s += "[]";
+                  }
+                  paramDimensionList.add(s);
+                  state = START;
+                  typeend = -1;
+                  dimension = 0;
+                  if (c == ',' || c == ')') {
+                    state = START;
+                  } else {
+                    state = NAME;
+                  }
+                }
+              }
+              break;
+            case NAME:
+              if (c == ',' || c == ')') {
+                state = START;
+              }
+              break;
+          }
+
+        }
+        params = paramList.toArray(new String[paramList.size()]);
+        paramDimensions = paramDimensionList.toArray(new String[paramList.size()]);
+        mem = mem.substring(0, index);
+      }
+    }
+
+    ClassInfo cl = null;
+    if (base instanceof ClassInfo) {
+      cl = (ClassInfo) base;
+    }
+
+    if (ref == null) {
+      // no class or package was provided, assume it's this class
+      if (cl != null) {
+        result.classInfo = cl;
+      }
+    } else {
+      // they provided something, maybe it's a class or a package
+      if (cl != null) {
+        result.classInfo = cl.extendedFindClass(ref);
+        if (result.classInfo == null) {
+          result.classInfo = cl.findClass(ref);
+        }
+        if (result.classInfo == null) {
+          result.classInfo = cl.findInnerClass(ref);
+        }
+      }
+      if (result.classInfo == null) {
+        result.classInfo = Converter.obtainClass(ref);
+      }
+      if (result.classInfo == null) {
+        result.packageInfo = Converter.obtainPackage(ref);
+      }
+    }
+
+    if (result.classInfo != null && mem != null) {
+      // it's either a field or a method, prefer a field
+      if (params == null) {
+        FieldInfo field = result.classInfo.findField(mem);
+        // findField looks in containing classes, so it might actually
+        // be somewhere else; link to where it really is, not what they
+        // typed.
+        if (field != null) {
+          result.classInfo = field.containingClass();
+          result.memberInfo = field;
+        }
+      }
+      if (result.memberInfo == null) {
+        MethodInfo method = result.classInfo.findMethod(mem, params, paramDimensions, varargs);
+        if (method != null) {
+          result.classInfo = method.containingClass();
+          result.memberInfo = method;
+        }
+      }
+    }
+
+    result.referencedMemberName = mem;
+    if (params != null) {
+      result.referencedMemberName = result.referencedMemberName + '(';
+      len = params.length;
+      if (len > 0) {
+        len--;
+        for (int i = 0; i < len; i++) {
+          result.referencedMemberName =
+              result.referencedMemberName + params[i] + paramDimensions[i] + ", ";
+        }
+        result.referencedMemberName =
+            result.referencedMemberName + params[len] + paramDimensions[len];
+      }
+      result.referencedMemberName = result.referencedMemberName + ")";
+    }
+
+    // debugging spew
+    if (false) {
+      result.label = result.label + "/" + ref + "/" + mem + '/';
+      if (params != null) {
+        for (int i = 0; i < params.length; i++) {
+          result.label += params[i] + "|";
+        }
+      }
+
+      FieldInfo f = (result.memberInfo instanceof FieldInfo) ? (FieldInfo) result.memberInfo : null;
+      MethodInfo m =
+          (result.memberInfo instanceof MethodInfo) ? (MethodInfo) result.memberInfo : null;
+      result.label =
+          result.label + "/package="
+              + (result.packageInfo != null ? result.packageInfo.name() : "") + "/class="
+              + (result.classInfo != null ? result.classInfo.qualifiedName() : "") + "/field="
+              + (f != null ? f.name() : "") + "/method=" + (m != null ? m.name() : "");
+
+    }
+
+    MethodInfo method = null;
+    boolean skipHref = false;
+
+    if (result.memberInfo != null && result.memberInfo.isExecutable()) {
+      method = (MethodInfo) result.memberInfo;
+    }
+
+    if (text.startsWith("\"")) {
+      // literal quoted reference (e.g., a book title)
+      Matcher matcher = QUOTE_PATTERN.matcher(text);
+      if (!matcher.matches()) {
+        Errors.error(Errors.UNRESOLVED_LINK, pos, "unbalanced quoted link/see tag: " + text.trim());
+        result.makeError();
+        return result;
+      }
+      skipHref = true;
+      result.label = matcher.group(1);
+      result.kind = "@seeJustLabel";
+    } else if (text.startsWith("<")) {
+      // explicit "<a href" form
+      Matcher matcher = HREF_PATTERN.matcher(text);
+      if (!matcher.matches()) {
+        Errors.error(Errors.UNRESOLVED_LINK, pos, "invalid <a> link/see tag: " + text.trim());
+        result.makeError();
+        return result;
+      }
+      result.href = matcher.group(1);
+      result.label = matcher.group(2);
+      result.kind = "@seeHref";
+    } else if (result.packageInfo != null) {
+      result.href = result.packageInfo.htmlPage();
+      if (result.label.length() == 0) {
+        result.href = result.packageInfo.htmlPage();
+        result.label = result.packageInfo.name();
+      }
+    } else if (result.classInfo != null && result.referencedMemberName == null) {
+      // class reference
+      if (result.label.length() == 0) {
+        result.label = result.classInfo.name();
+      }
+      result.href = result.classInfo.htmlPage();
+    } else if (result.memberInfo != null) {
+      // member reference
+      ClassInfo containing = result.memberInfo.containingClass();
+      if (result.memberInfo.isExecutable()) {
+        if (result.referencedMemberName.indexOf('(') < 0) {
+          result.referencedMemberName += method.flatSignature();
+        }
+      }
+      if (result.label.length() == 0) {
+        result.label = result.referencedMemberName;
+      }
+      result.href = containing.htmlPage() + '#' + result.memberInfo.anchor();
+    }
+
+    if (result.href == null && !skipHref) {
+      if (printOnErrors && (base == null || base.checkLevel())) {
+        Errors.error(Errors.UNRESOLVED_LINK, pos, "Unresolved link/see tag \"" + text.trim()
+            + "\" in " + ((base != null) ? base.qualifiedName() : "[null]"));
+      }
+      result.makeError();
+    } else if (result.memberInfo != null && !result.memberInfo.checkLevel()) {
+      if (printOnErrors && (base == null || base.checkLevel())) {
+        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden member: " + text.trim());
+        result.href = null;
+      }
+      result.kind = "@seeJustLabel";
+    } else if (result.classInfo != null && !result.classInfo.checkLevel()) {
+      if (printOnErrors && (base == null || base.checkLevel())) {
+        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden class: " + text.trim() + " label="
+            + result.label);
+        result.href = null;
+      }
+      result.kind = "@seeJustLabel";
+    } else if (result.packageInfo != null && !result.packageInfo.checkLevel()) {
+      if (printOnErrors && (base == null || base.checkLevel())) {
+        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden package: " + text.trim());
+        result.href = null;
+      }
+      result.kind = "@seeJustLabel";
+    }
+
+    result.good = true;
+
+    return result;
+  }
+
+  public boolean checkLevel() {
+    if (memberInfo != null) {
+      return memberInfo.checkLevel();
+    }
+    if (classInfo != null) {
+      return classInfo.checkLevel();
+    }
+    if (packageInfo != null) {
+      return packageInfo.checkLevel();
+    }
+    return false;
+  }
+
+  /** turn this LinkReference into one with an error message */
+  private void makeError() {
+    // this.href = "ERROR(" + this.text.trim() + ")";
+    this.href = null;
+    if (this.label == null) {
+      this.label = "";
+    }
+    this.label = "ERROR(" + this.label + "/" + text.trim() + ")";
+  }
+
+  /** private. **/
+  private LinkReference() {}
+}
diff --git a/src/com/google/doclava/LiteralTagInfo.java b/src/com/google/doclava/LiteralTagInfo.java
new file mode 100644
index 0000000..1feb276
--- /dev/null
+++ b/src/com/google/doclava/LiteralTagInfo.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class LiteralTagInfo extends TagInfo {
+  private static String encode(String t) {
+    t = t.replace("&", "&amp;");
+    t = t.replace("<", "&lt;");
+    t = t.replace(">", "&gt;");
+    return t;
+  }
+
+  public LiteralTagInfo(String text, SourcePositionInfo sp) {
+    super("Text", "Text", encode(text), sp);
+  }
+}
diff --git a/src/com/google/doclava/MemberInfo.java b/src/com/google/doclava/MemberInfo.java
new file mode 100644
index 0000000..9855e01
--- /dev/null
+++ b/src/com/google/doclava/MemberInfo.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public abstract class MemberInfo extends DocInfo implements Comparable, Scoped {
+  public MemberInfo(String rawCommentText, String name, String signature,
+      ClassInfo containingClass, ClassInfo realContainingClass, boolean isPublic,
+      boolean isProtected, boolean isPackagePrivate, boolean isPrivate, boolean isFinal,
+      boolean isStatic, boolean isSynthetic, String kind, SourcePositionInfo position,
+      AnnotationInstanceInfo[] annotations) {
+    super(rawCommentText, position);
+    mName = name;
+    mSignature = signature;
+    mContainingClass = containingClass;
+    mRealContainingClass = realContainingClass;
+    mIsPublic = isPublic;
+    mIsProtected = isProtected;
+    mIsPackagePrivate = isPackagePrivate;
+    mIsPrivate = isPrivate;
+    mIsFinal = isFinal;
+    mIsStatic = isStatic;
+    mIsSynthetic = isSynthetic;
+    mKind = kind;
+    mAnnotations = annotations;
+  }
+
+  public abstract boolean isExecutable();
+
+  public String anchor() {
+    if (mSignature != null) {
+      return mName + mSignature;
+    } else {
+      return mName;
+    }
+  }
+
+  public String htmlPage() {
+    return mContainingClass.htmlPage() + "#" + anchor();
+  }
+
+  public int compareTo(Object that) {
+    return this.htmlPage().compareTo(((MemberInfo) that).htmlPage());
+  }
+
+  public String name() {
+    return mName;
+  }
+
+  public String signature() {
+    return mSignature;
+  }
+
+  public ClassInfo realContainingClass() {
+    return mRealContainingClass;
+  }
+
+  public ClassInfo containingClass() {
+    return mContainingClass;
+  }
+
+  public boolean isPublic() {
+    return mIsPublic;
+  }
+
+  public boolean isProtected() {
+    return mIsProtected;
+  }
+
+  public boolean isPackagePrivate() {
+    return mIsPackagePrivate;
+  }
+
+  public boolean isPrivate() {
+    return mIsPrivate;
+  }
+  
+  public String scope() {
+    if (isPublic()) {
+      return "public";
+    } else if (isProtected()) {
+      return "protected";
+    } else if (isPackagePrivate()) {
+      return "";
+    } else if (isPrivate()) {
+      return "private";
+    } else {
+      throw new RuntimeException("invalid scope for object " + this);
+    }
+  }
+
+  public boolean isStatic() {
+    return mIsStatic;
+  }
+
+  public boolean isFinal() {
+    return mIsFinal;
+  }
+
+  public boolean isSynthetic() {
+    return mIsSynthetic;
+  }
+
+  @Override
+  public ContainerInfo parent() {
+    return mContainingClass;
+  }
+
+  public boolean checkLevel() {
+    return Doclava.checkLevel(mIsPublic, mIsProtected, mIsPackagePrivate, mIsPrivate, isHidden());
+  }
+
+  public String kind() {
+    return mKind;
+  }
+
+  public AnnotationInstanceInfo[] annotations() {
+    return mAnnotations;
+  }
+
+  ClassInfo mContainingClass;
+  ClassInfo mRealContainingClass;
+  String mName;
+  String mSignature;
+  boolean mIsPublic;
+  boolean mIsProtected;
+  boolean mIsPackagePrivate;
+  boolean mIsPrivate;
+  boolean mIsFinal;
+  boolean mIsStatic;
+  boolean mIsSynthetic;
+  String mKind;
+  private AnnotationInstanceInfo[] mAnnotations;
+
+}
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
new file mode 100644
index 0000000..c7e2304
--- /dev/null
+++ b/src/com/google/doclava/MethodInfo.java
@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+import com.google.doclava.apicheck.AbstractMethodInfo;
+
+import java.util.*;
+
+public class MethodInfo extends MemberInfo implements AbstractMethodInfo {
+  public static final Comparator<MethodInfo> comparator = new Comparator<MethodInfo>() {
+    public int compare(MethodInfo a, MethodInfo b) {
+      return a.name().compareTo(b.name());
+    }
+  };
+
+  private class InlineTags implements InheritedTags {
+    public TagInfo[] tags() {
+      return comment().tags();
+    }
+
+    public InheritedTags inherited() {
+      MethodInfo m = findOverriddenMethod(name(), signature());
+      if (m != null) {
+        return m.inlineTags();
+      } else {
+        return null;
+      }
+    }
+  }
+
+  private static void addInterfaces(ClassInfo[] ifaces, ArrayList<ClassInfo> queue) {
+    for (ClassInfo i : ifaces) {
+      queue.add(i);
+    }
+    for (ClassInfo i : ifaces) {
+      addInterfaces(i.interfaces(), queue);
+    }
+  }
+
+  // first looks for a superclass, and then does a breadth first search to
+  // find the least far away match
+  public MethodInfo findOverriddenMethod(String name, String signature) {
+    if (mReturnType == null) {
+      // ctor
+      return null;
+    }
+    if (mOverriddenMethod != null) {
+      return mOverriddenMethod;
+    }
+
+    ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
+    addInterfaces(containingClass().interfaces(), queue);
+    for (ClassInfo iface : queue) {
+      for (MethodInfo me : iface.methods()) {
+        if (me.name().equals(name) && me.signature().equals(signature)
+            && me.inlineTags().tags() != null && me.inlineTags().tags().length > 0) {
+          return me;
+        }
+      }
+    }
+    return null;
+  }
+
+  private static void addRealInterfaces(ClassInfo[] ifaces, ArrayList<ClassInfo> queue) {
+    for (ClassInfo i : ifaces) {
+      queue.add(i);
+      if (i.realSuperclass() != null && i.realSuperclass().isAbstract()) {
+        queue.add(i.superclass());
+      }
+    }
+    for (ClassInfo i : ifaces) {
+      addInterfaces(i.realInterfaces(), queue);
+    }
+  }
+
+  public MethodInfo findRealOverriddenMethod(String name, String signature, HashSet notStrippable) {
+    if (mReturnType == null) {
+      // ctor
+      return null;
+    }
+    if (mOverriddenMethod != null) {
+      return mOverriddenMethod;
+    }
+
+    ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
+    if (containingClass().realSuperclass() != null
+        && containingClass().realSuperclass().isAbstract()) {
+      queue.add(containingClass());
+    }
+    addInterfaces(containingClass().realInterfaces(), queue);
+    for (ClassInfo iface : queue) {
+      for (MethodInfo me : iface.methods()) {
+        if (me.name().equals(name) && me.signature().equals(signature)
+            && me.inlineTags().tags() != null && me.inlineTags().tags().length > 0
+            && notStrippable.contains(me.containingClass())) {
+          return me;
+        }
+      }
+    }
+    return null;
+  }
+
+  public MethodInfo findSuperclassImplementation(HashSet notStrippable) {
+    if (mReturnType == null) {
+      // ctor
+      return null;
+    }
+    if (mOverriddenMethod != null) {
+      // Even if we're told outright that this was the overridden method, we want to
+      // be conservative and ignore mismatches of parameter types -- they arise from
+      // extending generic specializations, and we want to consider the derived-class
+      // method to be a non-override.
+      if (this.signature().equals(mOverriddenMethod.signature())) {
+        return mOverriddenMethod;
+      }
+    }
+
+    ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
+    if (containingClass().realSuperclass() != null
+        && containingClass().realSuperclass().isAbstract()) {
+      queue.add(containingClass());
+    }
+    addInterfaces(containingClass().realInterfaces(), queue);
+    for (ClassInfo iface : queue) {
+      for (MethodInfo me : iface.methods()) {
+        if (me.name().equals(this.name()) && me.signature().equals(this.signature())
+            && notStrippable.contains(me.containingClass())) {
+          return me;
+        }
+      }
+    }
+    return null;
+  }
+
+  public ClassInfo findRealOverriddenClass(String name, String signature) {
+    if (mReturnType == null) {
+      // ctor
+      return null;
+    }
+    if (mOverriddenMethod != null) {
+      return mOverriddenMethod.mRealContainingClass;
+    }
+
+    ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
+    if (containingClass().realSuperclass() != null
+        && containingClass().realSuperclass().isAbstract()) {
+      queue.add(containingClass());
+    }
+    addInterfaces(containingClass().realInterfaces(), queue);
+    for (ClassInfo iface : queue) {
+      for (MethodInfo me : iface.methods()) {
+        if (me.name().equals(name) && me.signature().equals(signature)
+            && me.inlineTags().tags() != null && me.inlineTags().tags().length > 0) {
+          return iface;
+        }
+      }
+    }
+    return null;
+  }
+
+  private class FirstSentenceTags implements InheritedTags {
+    public TagInfo[] tags() {
+      return comment().briefTags();
+    }
+
+    public InheritedTags inherited() {
+      MethodInfo m = findOverriddenMethod(name(), signature());
+      if (m != null) {
+        return m.firstSentenceTags();
+      } else {
+        return null;
+      }
+    }
+  }
+
+  private class ReturnTags implements InheritedTags {
+    public TagInfo[] tags() {
+      return comment().returnTags();
+    }
+
+    public InheritedTags inherited() {
+      MethodInfo m = findOverriddenMethod(name(), signature());
+      if (m != null) {
+        return m.returnTags();
+      } else {
+        return null;
+      }
+    }
+  }
+
+  public boolean isDeprecated() {
+    boolean deprecated = false;
+    if (!mDeprecatedKnown) {
+      boolean commentDeprecated = comment().isDeprecated();
+      boolean annotationDeprecated = false;
+      for (AnnotationInstanceInfo annotation : annotations()) {
+        if (annotation.type().qualifiedName().equals("java.lang.Deprecated")) {
+          annotationDeprecated = true;
+          break;
+        }
+      }
+
+      if (commentDeprecated != annotationDeprecated) {
+        Errors.error(Errors.DEPRECATION_MISMATCH, position(), "Method "
+            + mContainingClass.qualifiedName() + "." + name()
+            + ": @Deprecated annotation and @deprecated doc tag do not match");
+      }
+
+      mIsDeprecated = commentDeprecated | annotationDeprecated;
+      mDeprecatedKnown = true;
+    }
+    return mIsDeprecated;
+  }
+  
+  public void setDeprecated(boolean deprecated) {
+    mDeprecatedKnown = true;
+    mIsDeprecated = deprecated;
+  }
+
+  public TypeInfo[] getTypeParameters() {
+    return mTypeParameters;
+  }
+
+  public MethodInfo cloneForClass(ClassInfo newContainingClass) {
+    MethodInfo result =
+        new MethodInfo(getRawCommentText(), mTypeParameters, name(), signature(),
+            newContainingClass, realContainingClass(), isPublic(), isProtected(),
+            isPackagePrivate(), isPrivate(), isFinal(), isStatic(), isSynthetic(), mIsAbstract,
+            mIsSynchronized, mIsNative, mIsAnnotationElement, kind(), mFlatSignature,
+            mOverriddenMethod, mReturnType, mParameters, mThrownExceptions, position(),
+            annotations());
+    result.init(mDefaultAnnotationElementValue);
+    return result;
+  }
+
+  public MethodInfo(String rawCommentText, TypeInfo[] typeParameters, String name,
+      String signature, ClassInfo containingClass, ClassInfo realContainingClass, boolean isPublic,
+      boolean isProtected, boolean isPackagePrivate, boolean isPrivate, boolean isFinal,
+      boolean isStatic, boolean isSynthetic, boolean isAbstract, boolean isSynchronized,
+      boolean isNative, boolean isAnnotationElement, String kind, String flatSignature,
+      MethodInfo overriddenMethod, TypeInfo returnType, ParameterInfo[] parameters,
+      ClassInfo[] thrownExceptions, SourcePositionInfo position,
+      AnnotationInstanceInfo[] annotations) {
+    // Explicitly coerce 'final' state of Java6-compiled enum values() method, to match
+    // the Java5-emitted base API description.
+    super(rawCommentText, name, signature, containingClass, realContainingClass, isPublic,
+        isProtected, isPackagePrivate, isPrivate, ((name.equals("values") && containingClass
+            .isEnum()) ? true : isFinal), isStatic, isSynthetic, kind, position, annotations);
+
+    // The underlying MethodDoc for an interface's declared methods winds up being marked
+    // non-abstract. Correct that here by looking at the immediate-parent class, and marking
+    // this method abstract if it is an unimplemented interface method.
+    if (containingClass.isInterface()) {
+      isAbstract = true;
+    }
+
+    mReasonOpened = "0:0";
+    mIsAnnotationElement = isAnnotationElement;
+    mTypeParameters = typeParameters;
+    mIsAbstract = isAbstract;
+    mIsSynchronized = isSynchronized;
+    mIsNative = isNative;
+    mFlatSignature = flatSignature;
+    mOverriddenMethod = overriddenMethod;
+    mReturnType = returnType;
+    mParameters = parameters;
+    mThrownExceptions = thrownExceptions;
+  }
+
+  public void init(AnnotationValueInfo defaultAnnotationElementValue) {
+    mDefaultAnnotationElementValue = defaultAnnotationElementValue;
+  }
+
+  public boolean isAbstract() {
+    return mIsAbstract;
+  }
+
+  public boolean isSynchronized() {
+    return mIsSynchronized;
+  }
+
+  public boolean isNative() {
+    return mIsNative;
+  }
+
+  public String flatSignature() {
+    return mFlatSignature;
+  }
+
+  public InheritedTags inlineTags() {
+    return new InlineTags();
+  }
+
+  public InheritedTags firstSentenceTags() {
+    return new FirstSentenceTags();
+  }
+
+  public InheritedTags returnTags() {
+    return new ReturnTags();
+  }
+
+  public TypeInfo returnType() {
+    return mReturnType;
+  }
+  
+  public String prettySignature() {
+    return name() + prettyParameters();
+  }
+  
+  /**
+   * Returns a printable version of the parameters of this method's signature.
+   */
+  public String prettyParameters() {
+    StringBuilder params = new StringBuilder("(");
+    for (ParameterInfo pInfo : mParameters) {
+      if (params.length() > 1) {
+        params.append(",");
+      }
+      params.append(pInfo.type().simpleTypeName());
+    }
+    
+    params.append(")");
+    return params.toString();
+  }
+
+  /**
+   * Returns a name consistent with the {@link com.google.doclava.MethodInfo#getHashableName()}.
+   */
+  public String getHashableName() {
+    StringBuilder result = new StringBuilder();
+    result.append(name());
+    for (int p = 0; p < mParameters.length; p++) {
+      result.append(":");
+      if (p == mParameters.length - 1 && isVarArgs()) {
+        // TODO: note that this does not attempt to handle hypothetical
+        // vararg methods whose last parameter is a list of arrays, e.g.
+        // "Object[]...".
+        result.append(mParameters[p].type().fullNameNoDimension(typeVariables())).append("...");
+      } else {
+        result.append(mParameters[p].type().fullName(typeVariables()));
+      }
+    }
+    return result.toString();
+  }
+
+  private boolean inList(ClassInfo item, ThrowsTagInfo[] list) {
+    int len = list.length;
+    String qn = item.qualifiedName();
+    for (int i = 0; i < len; i++) {
+      ClassInfo ex = list[i].exception();
+      if (ex != null && ex.qualifiedName().equals(qn)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public ThrowsTagInfo[] throwsTags() {
+    if (mThrowsTags == null) {
+      ThrowsTagInfo[] documented = comment().throwsTags();
+      ArrayList<ThrowsTagInfo> rv = new ArrayList<ThrowsTagInfo>();
+
+      int len = documented.length;
+      for (int i = 0; i < len; i++) {
+        rv.add(documented[i]);
+      }
+
+      ClassInfo[] all = mThrownExceptions;
+      len = all.length;
+      for (int i = 0; i < len; i++) {
+        ClassInfo cl = all[i];
+        if (documented == null || !inList(cl, documented)) {
+          rv.add(new ThrowsTagInfo("@throws", "@throws", cl.qualifiedName(), cl, "",
+              containingClass(), position()));
+        }
+      }
+      mThrowsTags = rv.toArray(new ThrowsTagInfo[rv.size()]);
+    }
+    return mThrowsTags;
+  }
+
+  private static int indexOfParam(String name, String[] list) {
+    final int N = list.length;
+    for (int i = 0; i < N; i++) {
+      if (name.equals(list[i])) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  public ParamTagInfo[] paramTags() {
+    if (mParamTags == null) {
+      final int N = mParameters.length;
+
+      String[] names = new String[N];
+      String[] comments = new String[N];
+      SourcePositionInfo[] positions = new SourcePositionInfo[N];
+
+      // get the right names so we can handle our names being different from
+      // our parent's names.
+      for (int i = 0; i < N; i++) {
+        names[i] = mParameters[i].name();
+        comments[i] = "";
+        positions[i] = mParameters[i].position();
+      }
+
+      // gather our comments, and complain about misnamed @param tags
+      for (ParamTagInfo tag : comment().paramTags()) {
+        int index = indexOfParam(tag.parameterName(), names);
+        if (index >= 0) {
+          comments[index] = tag.parameterComment();
+          positions[index] = tag.position();
+        } else {
+          Errors.error(Errors.UNKNOWN_PARAM_TAG_NAME, tag.position(),
+              "@param tag with name that doesn't match the parameter list: '" + tag.parameterName()
+                  + "'");
+        }
+      }
+
+      // get our parent's tags to fill in the blanks
+      MethodInfo overridden = this.findOverriddenMethod(name(), signature());
+      if (overridden != null) {
+        ParamTagInfo[] maternal = overridden.paramTags();
+        for (int i = 0; i < N; i++) {
+          if (comments[i].equals("")) {
+            comments[i] = maternal[i].parameterComment();
+            positions[i] = maternal[i].position();
+          }
+        }
+      }
+
+      // construct the results, and cache them for next time
+      mParamTags = new ParamTagInfo[N];
+      for (int i = 0; i < N; i++) {
+        mParamTags[i] =
+            new ParamTagInfo("@param", "@param", names[i] + " " + comments[i], parent(),
+                positions[i]);
+
+        // while we're here, if we find any parameters that are still undocumented at this
+        // point, complain. (this warning is off by default, because it's really, really
+        // common; but, it's good to be able to enforce it)
+        if (comments[i].equals("")) {
+          Errors.error(Errors.UNDOCUMENTED_PARAMETER, positions[i], "Undocumented parameter '"
+              + names[i] + "' on method '" + name() + "'");
+        }
+      }
+    }
+    return mParamTags;
+  }
+
+  public SeeTagInfo[] seeTags() {
+    SeeTagInfo[] result = comment().seeTags();
+    if (result == null) {
+      if (mOverriddenMethod != null) {
+        result = mOverriddenMethod.seeTags();
+      }
+    }
+    return result;
+  }
+
+  public TagInfo[] deprecatedTags() {
+    TagInfo[] result = comment().deprecatedTags();
+    if (result.length == 0) {
+      if (comment().undeprecateTags().length == 0) {
+        if (mOverriddenMethod != null) {
+          result = mOverriddenMethod.deprecatedTags();
+        }
+      }
+    }
+    return result;
+  }
+
+  public ParameterInfo[] parameters() {
+    return mParameters;
+  }
+
+
+  public boolean matchesParams(String[] params, String[] dimensions, boolean varargs) {
+    if (mParamStrings == null) {
+      ParameterInfo[] mine = mParameters;
+      int len = mine.length;
+      if (len != params.length) {
+        return false;
+      }
+      for (int i = 0; i < len; i++) {
+        if (!mine[i].matchesDimension(dimensions[i], varargs)) {
+          return false;
+        }
+        TypeInfo myType = mine[i].type();
+        String qualifiedName = myType.qualifiedTypeName();
+        String realType = myType.isPrimitive() ? "" : myType.asClassInfo().qualifiedName();
+        String s = params[i];
+        int slen = s.length();
+        int qnlen = qualifiedName.length();
+        
+        // Check for a matching generic name or best known type
+        if (!matchesType(qualifiedName, s) && !matchesType(realType, s)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+  
+  /**
+   * Checks to see if a parameter from a method signature is
+   * compatible with a parameter given in a {@code @link} tag.
+   */
+  private boolean matchesType(String signatureParam, String callerParam) {
+    int signatureLength = signatureParam.length();
+    int callerLength = callerParam.length();
+    return ((signatureParam.equals(callerParam) || ((callerLength + 1) < signatureLength
+        && signatureParam.charAt(signatureLength - callerLength - 1) == '.'
+        && signatureParam.endsWith(callerParam))));
+  }
+
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".kind", kind());
+    data.setValue(base + ".name", name());
+    data.setValue(base + ".href", htmlPage());
+    data.setValue(base + ".anchor", anchor());
+
+    if (mReturnType != null) {
+      returnType().makeHDF(data, base + ".returnType", false, typeVariables());
+      data.setValue(base + ".abstract", mIsAbstract ? "abstract" : "");
+    }
+
+    data.setValue(base + ".synchronized", mIsSynchronized ? "synchronized" : "");
+    data.setValue(base + ".final", isFinal() ? "final" : "");
+    data.setValue(base + ".static", isStatic() ? "static" : "");
+
+    TagInfo.makeHDF(data, base + ".shortDescr", firstSentenceTags());
+    TagInfo.makeHDF(data, base + ".descr", inlineTags());
+    TagInfo.makeHDF(data, base + ".deprecated", deprecatedTags());
+    TagInfo.makeHDF(data, base + ".seeAlso", seeTags());
+    data.setValue(base + ".since", getSince());
+    ParamTagInfo.makeHDF(data, base + ".paramTags", paramTags());
+    AttrTagInfo.makeReferenceHDF(data, base + ".attrRefs", comment().attrTags());
+    ThrowsTagInfo.makeHDF(data, base + ".throws", throwsTags());
+    ParameterInfo.makeHDF(data, base + ".params", parameters(), isVarArgs(), typeVariables());
+    if (isProtected()) {
+      data.setValue(base + ".scope", "protected");
+    } else if (isPublic()) {
+      data.setValue(base + ".scope", "public");
+    }
+    TagInfo.makeHDF(data, base + ".returns", returnTags());
+
+    if (mTypeParameters != null) {
+      TypeInfo.makeHDF(data, base + ".generic.typeArguments", mTypeParameters, false);
+    }
+    
+    setFederatedReferences(data, base);
+  }
+
+  public HashSet<String> typeVariables() {
+    HashSet<String> result = TypeInfo.typeVariables(mTypeParameters);
+    ClassInfo cl = containingClass();
+    while (cl != null) {
+      TypeInfo[] types = cl.asTypeInfo().typeArguments();
+      if (types != null) {
+        TypeInfo.typeVariables(types, result);
+      }
+      cl = cl.containingClass();
+    }
+    return result;
+  }
+
+  @Override
+  public boolean isExecutable() {
+    return true;
+  }
+
+  public ClassInfo[] thrownExceptions() {
+    return mThrownExceptions;
+  }
+
+  public String typeArgumentsName(HashSet<String> typeVars) {
+    if (mTypeParameters == null || mTypeParameters.length == 0) {
+      return "";
+    } else {
+      return TypeInfo.typeArgumentsName(mTypeParameters, typeVars);
+    }
+  }
+
+  public boolean isAnnotationElement() {
+    return mIsAnnotationElement;
+  }
+
+  public AnnotationValueInfo defaultAnnotationElementValue() {
+    return mDefaultAnnotationElementValue;
+  }
+
+  public void setVarargs(boolean set) {
+    mIsVarargs = set;
+  }
+
+  public boolean isVarArgs() {
+    return mIsVarargs;
+  }
+
+  @Override
+  public String toString() {
+    return this.name();
+  }
+
+  public void setReason(String reason) {
+    mReasonOpened = reason;
+  }
+
+  public String getReason() {
+    return mReasonOpened;
+  }
+  
+  public void addException(String exec) {
+    ClassInfo exceptionClass = new ClassInfo(exec);
+    List<ClassInfo> exceptions = new ArrayList<ClassInfo>(mThrownExceptions.length + 1);
+    exceptions.addAll(Arrays.asList(mThrownExceptions));
+    exceptions.add(exceptionClass);
+    mThrownExceptions = new ClassInfo[exceptions.size()];
+    exceptions.toArray(mThrownExceptions);
+  }
+  
+  public void addParameter(ParameterInfo p) {
+    // Name information
+    ParameterInfo[] newParams;
+    int i = 0;
+    
+    if (mParameters == null) {
+      newParams = new ParameterInfo[1];
+    } else {
+      newParams = new ParameterInfo[mParameters.length+1];
+      for (ParameterInfo info : mParameters) {
+        newParams[i++] = info;
+      }
+    }
+    newParams[i] = p;
+    mParameters = newParams;
+    
+    // Type information
+    TypeInfo[] newTypes;
+    i = 0;
+    
+    if (mTypeParameters == null) {
+      newTypes = new TypeInfo[1];
+    } else {
+      newTypes = new TypeInfo[mTypeParameters.length+1];
+      for (TypeInfo info : mTypeParameters) {
+        newTypes[i++] = info;
+      }
+    }
+    newTypes[i] = p.mType;
+    mTypeParameters = newTypes;
+  }
+
+  private String mFlatSignature;
+  private MethodInfo mOverriddenMethod;
+  private TypeInfo mReturnType;
+  private boolean mIsAnnotationElement;
+  private boolean mIsAbstract;
+  private boolean mIsSynchronized;
+  private boolean mIsNative;
+  private boolean mIsVarargs;
+  private boolean mDeprecatedKnown;
+  private boolean mIsDeprecated;
+  private ParameterInfo[] mParameters;
+  private ClassInfo[] mThrownExceptions;
+  private String[] mParamStrings;
+  ThrowsTagInfo[] mThrowsTags;
+  private ParamTagInfo[] mParamTags;
+  private TypeInfo[] mTypeParameters;
+  private AnnotationValueInfo mDefaultAnnotationElementValue;
+  private String mReasonOpened;
+  
+  // TODO: merge with droiddoc version (above)  
+  public String qualifiedName() {
+    String parentQName = (containingClass() != null)
+        ? (containingClass().qualifiedName() + ".") : "";
+    return parentQName + name();
+  }
+
+  @Override
+  public String signature() {
+    if (mSignature == null) {
+      StringBuilder params = new StringBuilder("(");
+      for (ParameterInfo pInfo : mParameters) {
+        if (params.length() > 1) {
+          params.append(", ");
+        }
+        params.append(pInfo.type().fullName());
+      }
+      
+      params.append(")");
+      mSignature = params.toString();
+    }
+    return mSignature;
+  }
+
+  public boolean matches(MethodInfo other) {
+    return signature().equals(other.signature());
+  }
+
+  public boolean throwsException(ClassInfo exception) {
+    for (ClassInfo e : mThrownExceptions) {
+      if (e.qualifiedName().equals(exception.qualifiedName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  public boolean isConsistent(MethodInfo mInfo) {
+    boolean consistent = true;
+    if (!this.mReturnType.equals(mInfo.mReturnType)) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method " + mInfo.qualifiedName()
+          + " 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");
+    }
+
+    if (mIsNative != mInfo.mIsNative) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_NATIVE, mInfo.position(), "Method " + mInfo.qualifiedName()
+          + " has changed 'native' qualifier");
+    }
+
+    if (mIsFinal != mInfo.mIsFinal) {
+      // Compiler-generated methods vary in their 'final' qual between versions of
+      // the compiler, so this check needs to be quite narrow. A change in 'final'
+      // status of a method is only relevant if (a) the method is not declared 'static'
+      // and (b) the method's class is not itself 'final'.
+      if (!mIsStatic) {
+        if ((containingClass() == null) || (!containingClass().isFinal())) {
+          consistent = false;
+          Errors.error(Errors.CHANGED_FINAL, mInfo.position(), "Method " + mInfo.qualifiedName()
+              + " has changed 'final' qualifier");
+        }
+      }
+    }
+
+    if (mIsStatic != mInfo.mIsStatic) {
+      consistent = false;
+      Errors.error(Errors.CHANGED_STATIC, mInfo.position(), "Method " + mInfo.qualifiedName()
+          + " 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());
+    }
+
+    if (!isDeprecated() == mInfo.isDeprecated()) {
+      Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), "Method " + mInfo.qualifiedName()
+          + " has changed deprecation state");
+      consistent = false;
+    }
+
+    if (mIsSynchronized != mInfo.mIsSynchronized) {
+      Errors.error(Errors.CHANGED_SYNCHRONIZED, mInfo.position(), "Method " + mInfo.qualifiedName()
+          + " has changed 'synchronized' qualifier from " + mIsSynchronized + " to "
+          + mInfo.mIsSynchronized);
+      consistent = false;
+    }
+
+    for (ClassInfo exception : thrownExceptions()) {
+      if (!mInfo.throwsException(exception)) {
+        // exclude 'throws' changes to finalize() overrides with no arguments
+        if (!name().equals("finalize") || (mParameters.length > 0)) {
+          Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Method " + mInfo.qualifiedName()
+              + " no longer throws exception " + exception.qualifiedName());
+          consistent = false;
+        }
+      }
+    }
+
+    for (ClassInfo exec : mInfo.thrownExceptions()) {
+      // exclude 'throws' changes to finalize() overrides with no arguments
+      if (!throwsException(exec)) {
+        if (!name().equals("finalize") || (mParameters.length > 0)) {
+          Errors.error(Errors.CHANGED_THROWS, mInfo.position(), "Method " + mInfo.qualifiedName()
+              + " added thrown exception " + exec.qualifiedName());
+          consistent = false;
+        }
+      }
+    }
+
+    return consistent;
+  }
+}
diff --git a/src/com/google/doclava/NavTree.java b/src/com/google/doclava/NavTree.java
new file mode 100644
index 0000000..5c3e53d
--- /dev/null
+++ b/src/com/google/doclava/NavTree.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NavTree {
+
+  public static void writeNavTree(String dir) {
+    List<Node> children = new ArrayList<Node>();
+    for (PackageInfo pkg : Doclava.choosePackages()) {
+      children.add(makePackageNode(pkg));
+    }
+    Node node = new Node("Reference", dir + "packages.html", children, null);
+
+    StringBuilder buf = new StringBuilder();
+    if (false) {
+      // if you want a root node
+      buf.append("[");
+      node.render(buf);
+      buf.append("]");
+    } else {
+      // if you don't want a root node
+      node.renderChildren(buf);
+    }
+
+    Data data = Doclava.makeHDF();
+    data.setValue("reference_tree", buf.toString());
+    ClearPage.write(data, "navtree_data.cs", "navtree_data.js");
+  }
+
+  private static Node makePackageNode(PackageInfo pkg) {
+    List<Node> children = new ArrayList<Node>();
+
+    children.add(new Node("Description", pkg.fullDescriptionHtmlPage(), null, null));
+
+    addClassNodes(children, "Interfaces", pkg.interfaces());
+    addClassNodes(children, "Classes", pkg.ordinaryClasses());
+    addClassNodes(children, "Enums", pkg.enums());
+    addClassNodes(children, "Exceptions", pkg.exceptions());
+    addClassNodes(children, "Errors", pkg.errors());
+
+    return new Node(pkg.name(), pkg.htmlPage(), children, pkg.getSince());
+  }
+
+  private static void addClassNodes(List<Node> parent, String label, ClassInfo[] classes) {
+    List<Node> children = new ArrayList<Node>();
+
+    for (ClassInfo cl : classes) {
+      if (cl.checkLevel()) {
+        children.add(new Node(cl.name(), cl.htmlPage(), null, cl.getSince()));
+      }
+    }
+
+    if (children.size() > 0) {
+      parent.add(new Node(label, null, children, null));
+    }
+  }
+
+  private static class Node {
+    private String mLabel;
+    private String mLink;
+    List<Node> mChildren;
+    private String mSince;
+
+    Node(String label, String link, List<Node> children, String since) {
+      mLabel = label;
+      mLink = link;
+      mChildren = children;
+      mSince = since;
+    }
+
+    static void renderString(StringBuilder buf, String s) {
+      if (s == null) {
+        buf.append("null");
+      } else {
+        buf.append('"');
+        final int N = s.length();
+        for (int i = 0; i < N; i++) {
+          char c = s.charAt(i);
+          if (c >= ' ' && c <= '~' && c != '"' && c != '\\') {
+            buf.append(c);
+          } else {
+            buf.append("\\u");
+            for (int j = 0; i < 4; i++) {
+              char x = (char) (c & 0x000f);
+              if (x > 10) {
+                x = (char) (x - 10 + 'a');
+              } else {
+                x = (char) (x + '0');
+              }
+              buf.append(x);
+              c >>= 4;
+            }
+          }
+        }
+        buf.append('"');
+      }
+    }
+
+    void renderChildren(StringBuilder buf) {
+      List<Node> list = mChildren;
+      if (list == null || list.size() == 0) {
+        // We output null for no children. That way empty lists here can just
+        // be a byproduct of how we generate the lists.
+        buf.append("null");
+      } else {
+        buf.append("[ ");
+        final int N = list.size();
+        for (int i = 0; i < N; i++) {
+          list.get(i).render(buf);
+          if (i != N - 1) {
+            buf.append(", ");
+          }
+        }
+        buf.append(" ]\n");
+      }
+    }
+
+    void render(StringBuilder buf) {
+      buf.append("[ ");
+      renderString(buf, mLabel);
+      buf.append(", ");
+      renderString(buf, mLink);
+      buf.append(", ");
+      renderChildren(buf);
+      buf.append(", ");
+      renderString(buf, mSince);
+      buf.append(" ]");
+    }
+  }
+}
diff --git a/src/com/google/doclava/PackageInfo.java b/src/com/google/doclava/PackageInfo.java
new file mode 100644
index 0000000..7651b5c
--- /dev/null
+++ b/src/com/google/doclava/PackageInfo.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import com.sun.javadoc.*;
+import java.util.*;
+
+public class PackageInfo extends DocInfo implements ContainerInfo {
+  public static final String DEFAULT_PACKAGE = "default package";
+  
+  public static final Comparator<PackageInfo> comparator = new Comparator<PackageInfo>() {
+    public int compare(PackageInfo a, PackageInfo b) {
+      return a.name().compareTo(b.name());
+    }
+  };
+
+  public PackageInfo(PackageDoc pkg, String name, SourcePositionInfo position) {
+    super(pkg.getRawCommentText(), position);
+    if (name.isEmpty()) {
+      mName = DEFAULT_PACKAGE;
+    } else {
+      mName = name;
+    }
+
+    mPackage = pkg;
+  }
+  
+  public PackageInfo(String name) {
+    super("", null);
+    mName = name;
+  }
+  
+  public PackageInfo(String name, SourcePositionInfo position) {
+    super("", position);
+    
+    if (name.isEmpty()) {
+      mName = "default package";
+    } else {
+      mName = name;
+    }
+  }
+
+  public String htmlPage() {
+    String s = mName;
+    s = s.replace('.', '/');
+    s += "/package-summary.html";
+    s = Doclava.javadocDir + s;
+    return s;
+  }
+
+  public String fullDescriptionHtmlPage() {
+    String s = mName;
+    s = s.replace('.', '/');
+    s += "/package-descr.html";
+    s = Doclava.javadocDir + s;
+    return s;
+  }
+
+  @Override
+  public ContainerInfo parent() {
+    return null;
+  }
+
+  @Override
+  public boolean isHidden() {
+    return comment().isHidden();
+  }
+
+  public boolean checkLevel() {
+    // TODO should return false if all classes are hidden but the package isn't.
+    // We don't have this so I'm not doing it now.
+    return !isHidden();
+  }
+
+  public String name() {
+    return mName;
+  }
+
+  public String qualifiedName() {
+    return mName;
+  }
+
+  public TagInfo[] inlineTags() {
+    return comment().tags();
+  }
+
+  public TagInfo[] firstSentenceTags() {
+    return comment().briefTags();
+  }
+
+  public static ClassInfo[] filterHidden(ClassInfo[] classes) {
+    ArrayList<ClassInfo> out = new ArrayList<ClassInfo>();
+
+    for (ClassInfo cl : classes) {
+      if (!cl.isHidden()) {
+        out.add(cl);
+      }
+    }
+
+    return out.toArray(new ClassInfo[0]);
+  }
+
+  public void makeLink(Data data, String base) {
+    if (checkLevel()) {
+      data.setValue(base + ".link", htmlPage());
+    }
+    data.setValue(base + ".name", name());
+    data.setValue(base + ".since", getSince());
+  }
+
+  public void makeClassLinkListHDF(Data data, String base) {
+    makeLink(data, base);
+    ClassInfo.makeLinkListHDF(data, base + ".interfaces", interfaces());
+    ClassInfo.makeLinkListHDF(data, base + ".classes", ordinaryClasses());
+    ClassInfo.makeLinkListHDF(data, base + ".enums", enums());
+    ClassInfo.makeLinkListHDF(data, base + ".exceptions", exceptions());
+    ClassInfo.makeLinkListHDF(data, base + ".errors", errors());
+    data.setValue(base + ".since", getSince());
+  }
+
+  public ClassInfo[] interfaces() {
+    if (mInterfaces == null) {
+      mInterfaces =
+          ClassInfo.sortByName(filterHidden(Converter.convertClasses(mPackage.interfaces())));
+    }
+    return mInterfaces;
+  }
+
+  public ClassInfo[] ordinaryClasses() {
+    if (mOrdinaryClasses == null) {
+      mOrdinaryClasses =
+          ClassInfo.sortByName(filterHidden(Converter.convertClasses(mPackage.ordinaryClasses())));
+    }
+    return mOrdinaryClasses;
+  }
+
+  public ClassInfo[] enums() {
+    if (mEnums == null) {
+      mEnums = ClassInfo.sortByName(filterHidden(Converter.convertClasses(mPackage.enums())));
+    }
+    return mEnums;
+  }
+
+  public ClassInfo[] exceptions() {
+    if (mExceptions == null) {
+      mExceptions =
+          ClassInfo.sortByName(filterHidden(Converter.convertClasses(mPackage.exceptions())));
+    }
+    return mExceptions;
+  }
+
+  public ClassInfo[] errors() {
+    if (mErrors == null) {
+      mErrors = ClassInfo.sortByName(filterHidden(Converter.convertClasses(mPackage.errors())));
+    }
+    return mErrors;
+  }
+
+  // in hashed containers, treat the name as the key
+  @Override
+  public int hashCode() {
+    return mName.hashCode();
+  }
+
+  private String mName;
+  private PackageDoc mPackage;
+  private ClassInfo[] mInterfaces;
+  private ClassInfo[] mOrdinaryClasses;
+  private ClassInfo[] mEnums;
+  private ClassInfo[] mExceptions;
+  private ClassInfo[] mErrors;
+  
+  // TODO: Leftovers from ApiCheck that should be better merged.
+  private HashMap<String, ClassInfo> mClasses = new HashMap<String, ClassInfo>();
+  
+  public void addClass(ClassInfo cl) {
+    mClasses.put(cl.name(), cl);
+  }
+
+  public HashMap<String, ClassInfo> allClasses() {
+    return mClasses;
+  }
+  
+  public boolean isConsistent(PackageInfo pInfo) {
+    boolean consistent = true;
+    for (ClassInfo cInfo : mClasses.values()) {
+      if (pInfo.mClasses.containsKey(cInfo.name())) {
+        if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()))) {
+          consistent = false;
+        }
+      } else {
+        Errors.error(Errors.REMOVED_CLASS, cInfo.position(), "Removed public class "
+            + cInfo.qualifiedName());
+        consistent = false;
+      }
+    }
+    for (ClassInfo cInfo : pInfo.mClasses.values()) {
+      if (!mClasses.containsKey(cInfo.name())) {
+        Errors.error(Errors.ADDED_CLASS, cInfo.position(), "Added class " + cInfo.name()
+            + " to package " + pInfo.name());
+        consistent = false;
+      }
+    }
+    return consistent;
+  }
+}
diff --git a/src/com/google/doclava/ParamTagInfo.java b/src/com/google/doclava/ParamTagInfo.java
new file mode 100644
index 0000000..f8329bc
--- /dev/null
+++ b/src/com/google/doclava/ParamTagInfo.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+public class ParamTagInfo extends ParsedTagInfo {
+  static final Pattern PATTERN = Pattern.compile("([^ \t\r\n]+)[ \t\r\n]+(.*)", Pattern.DOTALL);
+
+  private boolean mIsTypeParameter;
+  private String mParameterComment;
+  private String mParameterName;
+
+  ParamTagInfo(String name, String kind, String text, ContainerInfo base, SourcePositionInfo sp) {
+    super(name, kind, text, base, sp);
+
+    Matcher m = PATTERN.matcher(text);
+    if (m.matches()) {
+      mParameterName = m.group(1);
+      mParameterComment = m.group(2);
+      int len = mParameterName.length();
+      mIsTypeParameter =
+          len > 2 && mParameterName.charAt(0) == '<' && mParameterName.charAt(len - 1) == '>';
+    } else {
+      mParameterName = text.trim();
+      mParameterComment = "";
+      mIsTypeParameter = false;
+    }
+    setCommentText(mParameterComment);
+  }
+
+  ParamTagInfo(String name, String kind, String text, boolean isTypeParameter,
+      String parameterComment, String parameterName, ContainerInfo base, SourcePositionInfo sp) {
+    super(name, kind, text, base, sp);
+    mIsTypeParameter = isTypeParameter;
+    mParameterComment = parameterComment;
+    mParameterName = parameterName;
+  }
+
+  public boolean isTypeParameter() {
+    return mIsTypeParameter;
+  }
+
+  public String parameterComment() {
+    return mParameterComment;
+  }
+
+  public String parameterName() {
+    return mParameterName;
+  }
+
+  @Override
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".name", parameterName());
+    data.setValue(base + ".isTypeParameter", isTypeParameter() ? "1" : "0");
+    TagInfo.makeHDF(data, base + ".comment", commentTags());
+  }
+
+  public static void makeHDF(Data data, String base, ParamTagInfo[] tags) {
+    for (int i = 0; i < tags.length; i++) {
+      // don't output if the comment is ""
+      if (!"".equals(tags[i].parameterComment())) {
+        tags[i].makeHDF(data, base + "." + i);
+      }
+    }
+  }
+}
diff --git a/src/com/google/doclava/ParameterInfo.java b/src/com/google/doclava/ParameterInfo.java
new file mode 100644
index 0000000..f4e39f0
--- /dev/null
+++ b/src/com/google/doclava/ParameterInfo.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.HashSet;
+
+public class ParameterInfo {
+  public ParameterInfo(String name, String typeName, TypeInfo type, boolean isVarArg,
+      SourcePositionInfo position) {
+    mName = name;
+    mTypeName = typeName;
+    mType = type;
+    mIsVarArg = isVarArg;
+    mPosition = position;
+  }
+
+  TypeInfo type() {
+    return mType;
+  }
+
+  String name() {
+    return mName;
+  }
+
+  String typeName() {
+    return mTypeName;
+  }
+
+  SourcePositionInfo position() {
+    return mPosition;
+  }
+  
+  boolean isVarArg() {
+    return mIsVarArg;
+  }
+
+  public void makeHDF(Data data, String base, boolean isLastVararg, HashSet<String> typeVariables) {
+    data.setValue(base + ".name", this.name());
+    type().makeHDF(data, base + ".type", isLastVararg, typeVariables);
+  }
+
+  public static void makeHDF(Data data, String base, ParameterInfo[] params, boolean isVararg,
+      HashSet<String> typeVariables) {
+    for (int i = 0; i < params.length; i++) {
+      params[i].makeHDF(data, base + "." + i, isVararg && (i == params.length - 1), typeVariables);
+    }
+  }
+  
+  /**
+   * Returns true if this parameter's dimension information agrees
+   * with the represented callee's dimension information.
+   */
+  public boolean matchesDimension(String dimension, boolean varargs) {
+    if (varargs) {
+      dimension += "[]";
+    }
+    return mType.dimension().equals(dimension);
+  }
+
+  String mName;
+  String mTypeName;
+  TypeInfo mType;
+  boolean mIsVarArg;
+  SourcePositionInfo mPosition;
+}
diff --git a/src/com/google/doclava/ParsedTagInfo.java b/src/com/google/doclava/ParsedTagInfo.java
new file mode 100755
index 0000000..bcb9230
--- /dev/null
+++ b/src/com/google/doclava/ParsedTagInfo.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.util.ArrayList;
+
+public class ParsedTagInfo extends TagInfo {
+  private ContainerInfo mContainer;
+  private String mCommentText;
+  private Comment mComment;
+
+  ParsedTagInfo(String name, String kind, String text, ContainerInfo base, SourcePositionInfo sp) {
+    super(name, kind, text, SourcePositionInfo.findBeginning(sp, text));
+    mContainer = base;
+    mCommentText = text;
+  }
+
+  public TagInfo[] commentTags() {
+    if (mComment == null) {
+      mComment = new Comment(mCommentText, mContainer, position());
+    }
+    return mComment.tags();
+  }
+
+  protected void setCommentText(String comment) {
+    mCommentText = comment;
+  }
+
+  public static <T extends ParsedTagInfo> TagInfo[] joinTags(T[] tags) {
+    ArrayList<TagInfo> list = new ArrayList<TagInfo>();
+    final int N = tags.length;
+    for (int i = 0; i < N; i++) {
+      TagInfo[] t = tags[i].commentTags();
+      final int M = t.length;
+      for (int j = 0; j < M; j++) {
+        list.add(t[j]);
+      }
+    }
+    return list.toArray(new TagInfo[list.size()]);
+  }
+}
diff --git a/src/com/google/doclava/Proofread.java b/src/com/google/doclava/Proofread.java
new file mode 100644
index 0000000..64a0a1e
--- /dev/null
+++ b/src/com/google/doclava/Proofread.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.io.IOException;
+import java.io.FileWriter;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+public class Proofread {
+  static FileWriter out = null;
+  static final Pattern WHITESPACE = Pattern.compile("\\r?\\n");
+  static final String INDENT = "        ";
+  static final String NEWLINE = "\n" + INDENT;
+
+  public static void initProofread(String filename) {
+    try {
+      out = new FileWriter(filename);
+      out.write("javadoc proofread file: " + filename + "\n");
+    } catch (IOException e) {
+      if (out != null) {
+        try {
+          out.close();
+        } catch (IOException ex) {}
+        out = null;
+      }
+      System.err.println("error opening file: " + filename);
+    }
+  }
+
+  public static void finishProofread(String filename) {
+    if (out == null) {
+      return;
+    }
+
+    try {
+      out.close();
+    } catch (IOException e) {}
+  }
+
+  public static void write(String s) {
+    if (out == null) {
+      return;
+    }
+    try {
+      out.write(s);
+    } catch (IOException e) {}
+  }
+
+  public static void writeIndented(String s) {
+    s = s.trim();
+    Matcher m = WHITESPACE.matcher(s);
+    s = m.replaceAll(NEWLINE);
+    write(INDENT);
+    write(s);
+    write("\n");
+  }
+
+  public static void writeFileHeader(String filename) {
+    write("\n\n=== ");
+    write(filename);
+    write(" ===\n");
+  }
+
+  public static void writeTagList(TagInfo[] tags) {
+    if (out == null) {
+      return;
+    }
+
+    for (TagInfo t : tags) {
+      String k = t.kind();
+      if ("Text".equals(t.name())) {
+        writeIndented(t.text());
+      } else if ("@more".equals(k)) {
+        writeIndented("");
+      } else if ("@see".equals(k)) {
+        SeeTagInfo see = (SeeTagInfo) t;
+        String label = see.label();
+        if (label == null) {
+          label = "";
+        }
+        writeIndented("{" + see.name() + " ... " + label + "}");
+      } else if ("@code".equals(k)) {
+        writeIndented(t.text());
+      } else if ("@samplecode".equals(k)) {
+        writeIndented(t.text());
+      } else {
+        writeIndented("{" + (t.name() != null ? t.name() : "") + "/" + t.text() + "}");
+      }
+    }
+  }
+
+  public static void writePackages(String filename, TagInfo[] tags) {
+    if (out == null) {
+      return;
+    }
+
+    writeFileHeader(filename);
+    writeTagList(tags);
+  }
+
+  public static void writePackage(String filename, TagInfo[] tags) {
+    if (out == null) {
+      return;
+    }
+
+    writeFileHeader(filename);
+    writeTagList(tags);
+  }
+
+  public static void writeClass(String filename, ClassInfo cl) {
+    if (out == null) {
+      return;
+    }
+
+    writeFileHeader(filename);
+    writeTagList(cl.inlineTags());
+
+    // enum constants
+    for (FieldInfo f : cl.enumConstants()) {
+      write("ENUM: " + f.name() + "\n");
+      writeTagList(f.inlineTags());
+    }
+
+    // fields
+    for (FieldInfo f : cl.selfFields()) {
+      write("FIELD: " + f.name() + "\n");
+      writeTagList(f.inlineTags());
+    }
+
+    // constructors
+    for (MethodInfo m : cl.constructors()) {
+      write("CONSTRUCTOR: " + m.name() + "\n");
+      writeTagList(m.inlineTags().tags());
+    }
+
+    // methods
+    for (MethodInfo m : cl.selfMethods()) {
+      write("METHOD: " + m.name() + "\n");
+      writeTagList(m.inlineTags().tags());
+    }
+  }
+}
diff --git a/src/com/google/doclava/SampleCode.java b/src/com/google/doclava/SampleCode.java
new file mode 100644
index 0000000..91b1746
--- /dev/null
+++ b/src/com/google/doclava/SampleCode.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.*;
+import java.io.*;
+
+
+public class SampleCode {
+  String mSource;
+  String mDest;
+  String mTitle;
+
+  public SampleCode(String source, String dest, String title) {
+    mSource = source;
+    mTitle = title;
+    int len = dest.length();
+    if (len > 1 && dest.charAt(len - 1) != '/') {
+      mDest = dest + '/';
+    } else {
+      mDest = dest;
+    }
+  }
+
+  public void write(boolean offlineMode) {
+    File f = new File(mSource);
+    if (!f.isDirectory()) {
+      System.out.println("-samplecode not a directory: " + mSource);
+      return;
+    }
+    if (offlineMode)
+      writeIndexOnly(f, mDest, offlineMode);
+    else
+      writeDirectory(f, mDest);
+  }
+
+  public static String convertExtension(String s, String ext) {
+    return s.substring(0, s.lastIndexOf('.')) + ext;
+  }
+
+  public static String[] IMAGES = {".png", ".jpg", ".gif"};
+  public static String[] TEMPLATED = {".java", ".xml"};
+
+  public static boolean inList(String s, String[] list) {
+    for (String t : list) {
+      if (s.endsWith(t)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public void writeDirectory(File dir, String relative) {
+    TreeSet<String> dirs = new TreeSet<String>();
+    TreeSet<String> files = new TreeSet<String>();
+
+    String subdir = relative; // .substring(mDest.length());
+
+    for (File f : dir.listFiles()) {
+      String name = f.getName();
+      if (name.startsWith(".") || name.startsWith("_")) {
+        continue;
+      }
+      if (f.isFile()) {
+        String out = relative + name;
+
+        if (inList(out, IMAGES)) {
+          // copied directly
+          ClearPage.copyFile(f, out);
+          writeImagePage(f, convertExtension(out, Doclava.htmlExtension), subdir);
+          files.add(name);
+        }
+        if (inList(out, TEMPLATED)) {
+          // copied and goes through the template
+          ClearPage.copyFile(f, out);
+          writePage(f, convertExtension(out, Doclava.htmlExtension), subdir);
+          files.add(name);
+        }
+        // else ignored
+      } else if (f.isDirectory()) {
+        writeDirectory(f, relative + name + "/");
+        dirs.add(name);
+      }
+    }
+
+    // write the index page
+    int i;
+
+    Data hdf = writeIndex(dir);
+    hdf.setValue("subdir", subdir);
+    i = 0;
+    for (String d : dirs) {
+      hdf.setValue("subdirs." + i + ".name", d);
+      i++;
+    }
+    i = 0;
+    for (String f : files) {
+      hdf.setValue("files." + i + ".name", f);
+      hdf.setValue("files." + i + ".href", convertExtension(f, ".html"));
+      i++;
+    }
+
+    ClearPage.write(hdf, "sampleindex.cs", relative + "/index" + Doclava.htmlExtension);
+  }
+
+  public void writeIndexOnly(File dir, String relative, Boolean offline) {
+    Data hdf = writeIndex(dir);
+    if (!offline) relative = "/" + relative;
+    ClearPage.write(hdf, "sampleindex.cs", relative + "index" + Doclava.htmlExtension);
+  }
+
+  public Data writeIndex(File dir) {
+    Data hdf = Doclava.makeHDF();
+
+    hdf.setValue("page.title", dir.getName() + " - " + mTitle);
+    hdf.setValue("projectTitle", mTitle);
+
+    String filename = dir.getPath() + "/_index.html";
+    String summary =
+        SampleTagInfo.readFile(new SourcePositionInfo(filename, -1, -1), filename, "sample code",
+            true, false, true);
+
+    if (summary == null) {
+      summary = "";
+    }
+    hdf.setValue("summary", summary);
+
+    return hdf;
+  }
+
+  public void writePage(File f, String out, String subdir) {
+    String name = f.getName();
+
+    String filename = f.getPath();
+    String data =
+        SampleTagInfo.readFile(new SourcePositionInfo(filename, -1, -1), filename, "sample code",
+            true, true, true);
+    data = Doclava.escape(data);
+
+    Data hdf = Doclava.makeHDF();
+
+    hdf.setValue("page.title", name);
+    hdf.setValue("subdir", subdir);
+    hdf.setValue("realFile", name);
+    hdf.setValue("fileContents", data);
+
+    ClearPage.write(hdf, "sample.cs", out);
+  }
+
+  public void writeImagePage(File f, String out, String subdir) {
+    String name = f.getName();
+
+    String data = "<img src=\"" + name + "\" title=\"" + name + "\" />";
+
+    Data hdf = Doclava.makeHDF();
+
+    hdf.setValue("page.title", name);
+    hdf.setValue("subdir", subdir);
+    hdf.setValue("realFile", name);
+    hdf.setValue("fileContents", data);
+
+    ClearPage.write(hdf, "sample.cs", out);
+  }
+}
diff --git a/src/com/google/doclava/SampleTagInfo.java b/src/com/google/doclava/SampleTagInfo.java
new file mode 100644
index 0000000..9b9e40a
--- /dev/null
+++ b/src/com/google/doclava/SampleTagInfo.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.io.Reader;
+import java.io.IOException;
+import java.io.FileReader;
+import java.io.LineNumberReader;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+/*
+ * SampleTagInfo copies text from a given file into the javadoc comment.
+ * 
+ * The @include tag copies the text verbatim from the given file.
+ * 
+ * The @sample tag copies the text from the given file, stripping leading and trailing whitespace,
+ * and reducing the indent level of the text to the indent level of the first non-whitespace line.
+ * 
+ * Both tags accept either a filename and an id or just a filename. If no id is provided, the entire
+ * file is copied. If an id is provided, the lines in the given file between the first two lines
+ * containing BEGIN_INCLUDE(id) and END_INCLUDE(id), for the given id, are copied. The id may be
+ * only letters, numbers and underscore (_).
+ * 
+ * Four examples: {@include samples/ApiDemos/src/com/google/app/Notification1.java} {@sample
+ * samples/ApiDemos/src/com/google/app/Notification1.java} {@include
+ * samples/ApiDemos/src/com/google/app/Notification1.java Bleh} {@sample
+ * samples/ApiDemos/src/com/google/app/Notification1.java Bleh}
+ */
+public class SampleTagInfo extends TagInfo {
+  static final int STATE_BEGIN = 0;
+  static final int STATE_MATCHING = 1;
+
+  static final Pattern TEXT =
+      Pattern.compile("[\r\n \t]*([^\r\n \t]*)[\r\n \t]*([0-9A-Za-z_]*)[\r\n \t]*", Pattern.DOTALL);
+
+  private static final String BEGIN_INCLUDE = "BEGIN_INCLUDE";
+  private static final String END_INCLUDE = "END_INCLUDE";
+
+  private ContainerInfo mBase;
+  private String mIncluded;
+
+  public static String escapeHtml(String str) {
+    return str.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
+  }
+
+  private static boolean isIncludeLine(String str) {
+    return str.indexOf(BEGIN_INCLUDE) >= 0 || str.indexOf(END_INCLUDE) >= 0;
+  }
+
+  SampleTagInfo(String name, String kind, String text, ContainerInfo base,
+      SourcePositionInfo position) {
+    super(name, kind, text, position);
+    mBase = base;
+
+    Matcher m = TEXT.matcher(text);
+    if (!m.matches()) {
+      Errors.error(Errors.BAD_INCLUDE_TAG, position, "Bad @include tag: " + text);
+      return;
+    }
+    String filename = m.group(1);
+    String id = m.group(2);
+    boolean trim = "@sample".equals(name);
+
+    if (id == null || "".equals(id)) {
+      mIncluded = readFile(position, filename, id, trim, true, false);
+    } else {
+      mIncluded = loadInclude(position, filename, id, trim);
+    }
+
+    if (mIncluded == null) {
+      Errors.error(Errors.BAD_INCLUDE_TAG, position, "include tag '" + id + "' not found in file: "
+          + filename);
+    }
+  }
+
+  static String getTrimString(String line) {
+    int i = 0;
+    int len = line.length();
+    for (; i < len; i++) {
+      char c = line.charAt(i);
+      if (c != ' ' && c != '\t') {
+        break;
+      }
+    }
+    if (i == len) {
+      return null;
+    } else {
+      return line.substring(0, i);
+    }
+  }
+
+  static String loadInclude(SourcePositionInfo pos, String filename, String id, boolean trim) {
+    Reader input = null;
+    StringBuilder result = new StringBuilder();
+
+    String begin = BEGIN_INCLUDE + "(" + id + ")";
+    String end = END_INCLUDE + "(" + id + ")";
+
+    try {
+      input = new FileReader(filename);
+      LineNumberReader lines = new LineNumberReader(input);
+
+      int state = STATE_BEGIN;
+
+      int trimLength = -1;
+      String trimString = null;
+      int trailing = 0;
+
+      while (true) {
+        String line = lines.readLine();
+        if (line == null) {
+          return null;
+        }
+        switch (state) {
+          case STATE_BEGIN:
+            if (line.indexOf(begin) >= 0) {
+              state = STATE_MATCHING;
+            }
+            break;
+          case STATE_MATCHING:
+            if (line.indexOf(end) >= 0) {
+              return result.substring(0);
+            } else {
+              boolean empty = "".equals(line.trim());
+              if (trim) {
+                if (isIncludeLine(line)) {
+                  continue;
+                }
+                if (trimLength < 0 && !empty) {
+                  trimString = getTrimString(line);
+                  if (trimString != null) {
+                    trimLength = trimString.length();
+                  }
+                }
+                if (trimLength >= 0 && line.length() > trimLength) {
+                  boolean trimThisLine = true;
+                  for (int i = 0; i < trimLength; i++) {
+                    if (line.charAt(i) != trimString.charAt(i)) {
+                      trimThisLine = false;
+                      break;
+                    }
+                  }
+                  if (trimThisLine) {
+                    line = line.substring(trimLength);
+                  }
+                }
+                if (trimLength >= 0) {
+                  if (!empty) {
+                    for (int i = 0; i < trailing; i++) {
+                      result.append('\n');
+                    }
+                    line = escapeHtml(line);
+                    result.append(line);
+                    trailing = 1; // add \n next time, maybe
+                  } else {
+                    trailing++;
+                  }
+                }
+              } else {
+                result.append(line);
+                result.append('\n');
+              }
+            }
+            break;
+        }
+      }
+    } catch (IOException e) {
+      Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Error reading file for" + " include \"" + id
+          + "\" " + filename);
+    } finally {
+      if (input != null) {
+        try {
+          input.close();
+        } catch (IOException ex) {}
+      }
+    }
+    Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Did not find " + end + " in file " + filename);
+    return null;
+  }
+
+  static String readFile(SourcePositionInfo pos, String filename, String id, boolean trim,
+      boolean escape, boolean errorOk) {
+    Reader input = null;
+    StringBuilder result = new StringBuilder();
+    int trailing = 0;
+    boolean started = false;
+    try {
+      input = new FileReader(filename);
+      LineNumberReader lines = new LineNumberReader(input);
+
+      while (true) {
+        String line = lines.readLine();
+        if (line == null) {
+          break;
+        }
+        if (trim) {
+          if (isIncludeLine(line)) {
+            continue;
+          }
+          if (!"".equals(line.trim())) {
+            if (started) {
+              for (int i = 0; i < trailing; i++) {
+                result.append('\n');
+              }
+            }
+            if (escape) {
+              line = escapeHtml(line);
+            }
+            result.append(line);
+            trailing = 1; // add \n next time, maybe
+            started = true;
+          } else {
+            if (started) {
+              trailing++;
+            }
+          }
+        } else {
+          result.append(line);
+          result.append('\n');
+        }
+      }
+    } catch (IOException e) {
+      if (errorOk) {
+        return null;
+      } else {
+        Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Error reading file for" + " include \"" + id
+            + "\" " + filename);
+      }
+    } finally {
+      if (input != null) {
+        try {
+          input.close();
+        } catch (IOException ex) {}
+      }
+    }
+    return result.substring(0);
+  }
+
+  @Override
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".name", name());
+    data.setValue(base + ".kind", kind());
+    if (mIncluded != null) {
+      data.setValue(base + ".text", mIncluded);
+    } else {
+      data.setValue(base + ".text", "INCLUDE_ERROR");
+    }
+  }
+}
diff --git a/src/com/google/doclava/Scoped.java b/src/com/google/doclava/Scoped.java
new file mode 100644
index 0000000..03e42f9
--- /dev/null
+++ b/src/com/google/doclava/Scoped.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public interface Scoped {
+  boolean isPublic();
+
+  boolean isProtected();
+
+  boolean isPackagePrivate();
+
+  boolean isPrivate();
+
+  boolean isHidden();
+}
diff --git a/src/com/google/doclava/SeeTagInfo.java b/src/com/google/doclava/SeeTagInfo.java
new file mode 100644
index 0000000..40581cd
--- /dev/null
+++ b/src/com/google/doclava/SeeTagInfo.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+public class SeeTagInfo extends TagInfo {
+  private ContainerInfo mBase;
+  LinkReference mLink;
+
+  SeeTagInfo(String name, String kind, String text, ContainerInfo base, SourcePositionInfo position) {
+    super(name, kind, text, position);
+    mBase = base;
+  }
+
+  protected LinkReference linkReference() {
+    if (mLink == null) {
+      mLink =
+          LinkReference.parse(text(), mBase, position(), (!"@see".equals(name()))
+              && (mBase != null ? mBase.checkLevel() : true));
+    }
+    return mLink;
+  }
+
+  public String label() {
+    return linkReference().label;
+  }
+
+  @Override
+  public void makeHDF(Data data, String base) {
+    LinkReference linkRef = linkReference();
+    if (linkRef.kind != null) {
+      // if they have a better suggestion about "kind" use that.
+      // do this before super.makeHDF() so it picks it up
+      setKind(linkRef.kind);
+    }
+
+    super.makeHDF(data, base);
+
+    data.setValue(base + ".label", linkRef.label);
+    if (linkRef.href != null) {
+      data.setValue(base + ".href", linkRef.href);
+    }
+  }
+
+  public boolean checkLevel() {
+    return linkReference().checkLevel();
+  }
+
+  public static void makeHDF(Data data, String base, SeeTagInfo[] tags) {
+    int j = 0;
+    for (SeeTagInfo tag : tags) {
+      if (tag.mBase.checkLevel() && tag.checkLevel()) {
+        tag.makeHDF(data, base + "." + j);
+        j++;
+      }
+    }
+  }
+}
diff --git a/src/com/google/doclava/SinceTagger.java b/src/com/google/doclava/SinceTagger.java
new file mode 100644
index 0000000..00e2029
--- /dev/null
+++ b/src/com/google/doclava/SinceTagger.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+import com.google.doclava.apicheck.ApiCheck;
+import com.google.doclava.apicheck.ApiInfo;
+import com.google.doclava.apicheck.ApiParseException;
+
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Collections;
+
+
+/**
+ * Applies version information to the DroidDoc class model from apicheck XML files. Sample usage:
+ * 
+ * <pre>
+ *   ClassInfo[] classInfos = ...
+ *
+ *   SinceTagger sinceTagger = new SinceTagger()
+ *   sinceTagger.addVersion("frameworks/base/api/1.xml", "Android 1.0")
+ *   sinceTagger.addVersion("frameworks/base/api/2.xml", "Android 1.5")
+ *   sinceTagger.tagAll(...);
+ * </pre>
+ */
+public class SinceTagger {
+
+  private final Map<String, String> xmlToName = new LinkedHashMap<String, String>();
+
+  /**
+   * Specifies the apicheck XML file and the API version it holds. Calls to this method should be
+   * called in order from oldest version to newest.
+   */
+  public void addVersion(String file, String name) {
+    xmlToName.put(file, name);
+  }
+
+  public void tagAll(ClassInfo[] classDocs) {
+    // read through the XML files in order, applying their since information
+    // to the Javadoc models
+    for (Map.Entry<String, String> versionSpec : xmlToName.entrySet()) {
+      String xmlFile = versionSpec.getKey();
+      String versionName = versionSpec.getValue();
+      
+      ApiInfo specApi;
+      try {
+        specApi = new ApiCheck().parseApi(xmlFile);
+      } catch (ApiParseException e) {
+        Errors.error(Errors.NO_SINCE_DATA, null, "Could not add since data for " + versionName);
+        continue;
+      }
+
+      applyVersionsFromSpec(versionName, specApi, classDocs);
+    }
+
+    if (!xmlToName.isEmpty()) {
+      warnForMissingVersions(classDocs);
+    }
+  }
+
+  public boolean hasVersions() {
+    return !xmlToName.isEmpty();
+  }
+
+  /**
+   * Writes an index of the version names to {@code data}.
+   */
+  public void writeVersionNames(Data data) {
+    int index = 1;
+    for (String version : xmlToName.values()) {
+      data.setValue("since." + index + ".name", version);
+      index++;
+    }
+  }
+
+  /**
+   * Applies the version information to {@code classDocs} where not already present.
+   * 
+   * @param versionName the version name
+   * @param specApi the spec for this version. If a symbol is in this spec, it was present in the
+   *        named version
+   * @param classDocs the doc model to update
+   */
+  private void applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs) {
+    for (ClassInfo classDoc : classDocs) {
+      PackageInfo packageSpec
+          = specApi.getPackages().get(classDoc.containingPackage().name());
+
+      if (packageSpec == null) {
+        continue;
+      }
+
+      ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name());
+
+      if (classSpec == null) {
+        continue;
+      }
+
+      versionPackage(versionName, classDoc.containingPackage());
+      versionClass(versionName, classDoc);
+      versionConstructors(versionName, classSpec, classDoc);
+      versionFields(versionName, classSpec, classDoc);
+      versionMethods(versionName, classSpec, classDoc);
+    }
+  }
+
+  /**
+   * Applies version information to {@code doc} where not already present.
+   */
+  private void versionPackage(String versionName, PackageInfo doc) {
+    if (doc.getSince() == null) {
+      doc.setSince(versionName);
+    }
+  }
+
+  /**
+   * Applies version information to {@code doc} where not already present.
+   */
+  private void versionClass(String versionName, ClassInfo doc) {
+    if (doc.getSince() == null) {
+      doc.setSince(versionName);
+    }
+  }
+
+  /**
+   * Applies version information from {@code spec} to {@code doc} where not already present.
+   */
+  private void versionConstructors(String versionName, ClassInfo spec, ClassInfo doc) {
+    for (MethodInfo constructor : doc.constructors()) {
+      if (constructor.getSince() == null
+          && spec.hasConstructor(constructor)) {
+        constructor.setSince(versionName);
+      }
+    }
+  }
+
+  /**
+   * Applies version information from {@code spec} to {@code doc} where not already present.
+   */
+  private void versionFields(String versionName, ClassInfo spec, ClassInfo doc) {
+    for (FieldInfo field : doc.fields()) {
+      if (field.getSince() == null && spec.allFields().containsKey(field.name())) {
+        field.setSince(versionName);
+      }
+    }
+  }
+
+  /**
+   * Applies version information from {@code spec} to {@code doc} where not already present.
+   */
+  private void versionMethods(String versionName, ClassInfo spec, ClassInfo doc) {
+    for (MethodInfo method : doc.methods()) {
+      if (method.getSince() != null) {
+        continue;
+      }
+
+      for (ClassInfo superclass : spec.hierarchy()) {
+        if (superclass.allMethods().containsKey(method.getHashableName())) {
+          method.setSince(versionName);
+          break;
+        }
+      }
+    }
+  }
+
+  /**
+   * Warns if any symbols are missing version information. When configured properly, this will yield
+   * zero warnings because {@code apicheck} guarantees that all symbols are present in the most
+   * recent API.
+   */
+  private void warnForMissingVersions(ClassInfo[] classDocs) {
+    for (ClassInfo claz : classDocs) {
+      if (!checkLevelRecursive(claz)) {
+        continue;
+      }
+
+      if (claz.getSince() == null) {
+        Errors.error(Errors.NO_SINCE_DATA, claz.position(), "XML missing class "
+            + claz.qualifiedName());
+      }
+
+      for (FieldInfo field : missingVersions(claz.fields())) {
+        Errors.error(Errors.NO_SINCE_DATA, field.position(), "XML missing field "
+            + claz.qualifiedName() + "#" + field.name());
+      }
+
+      for (MethodInfo constructor : missingVersions(claz.constructors())) {
+        Errors.error(Errors.NO_SINCE_DATA, constructor.position(), "XML missing constructor "
+            + claz.qualifiedName() + "#" + constructor.getHashableName());
+      }
+
+      for (MethodInfo method : missingVersions(claz.methods())) {
+        Errors.error(Errors.NO_SINCE_DATA, method.position(), "XML missing method "
+            + claz.qualifiedName() + "#" + method.getHashableName());
+      }
+    }
+  }
+
+  /**
+   * Returns the DocInfos in {@code all} that are documented but do not have since tags.
+   */
+  private <T extends MemberInfo> Iterable<T> missingVersions(T[] all) {
+    List<T> result = Collections.emptyList();
+    for (T t : all) {
+      // if this member has version info or isn't documented, skip it
+      if (t.getSince() != null || t.isHidden() || !checkLevelRecursive(t.realContainingClass())) {
+        continue;
+      }
+
+      if (result.isEmpty()) {
+        result = new ArrayList<T>(); // lazily construct a mutable list
+      }
+      result.add(t);
+    }
+    return result;
+  }
+
+  /**
+   * Returns true if {@code claz} and all containing classes are documented. The result may be used
+   * to filter out members that exist in the API data structure but aren't a part of the API.
+   */
+  private boolean checkLevelRecursive(ClassInfo claz) {
+    for (ClassInfo c = claz; c != null; c = c.containingClass()) {
+      if (!c.checkLevel()) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/com/google/doclava/Sorter.java b/src/com/google/doclava/Sorter.java
new file mode 100644
index 0000000..2ee8f67
--- /dev/null
+++ b/src/com/google/doclava/Sorter.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class Sorter implements Comparable {
+  public String label;
+  public Object data;
+
+  public Sorter(String l, Object d) {
+    label = l;
+    data = d;
+  }
+
+  public int compareTo(Object other) {
+    return label.compareToIgnoreCase(((Sorter) other).label);
+  }
+}
diff --git a/src/com/google/doclava/SourcePositionInfo.java b/src/com/google/doclava/SourcePositionInfo.java
new file mode 100644
index 0000000..cf516c8
--- /dev/null
+++ b/src/com/google/doclava/SourcePositionInfo.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class SourcePositionInfo implements Comparable {
+  public static final SourcePositionInfo UNKNOWN = new SourcePositionInfo("(unknown)", 0, 0);
+
+  public SourcePositionInfo(String file, int line, int column) {
+    this.file = file;
+    this.line = line;
+    this.column = column;
+  }
+
+  public SourcePositionInfo(SourcePositionInfo that) {
+    this.file = that.file;
+    this.line = that.line;
+    this.column = that.column;
+  }
+
+  /**
+   * Given this position and str which occurs at that position, as well as str an index into str,
+   * find the SourcePositionInfo.
+   * 
+   * @throw StringIndexOutOfBoundsException if index &gt; str.length()
+   */
+  public static SourcePositionInfo add(SourcePositionInfo that, String str, int index) {
+    if (that == null) {
+      return null;
+    }
+    int line = that.line;
+    char prev = 0;
+    for (int i = 0; i < index; i++) {
+      char c = str.charAt(i);
+      if (c == '\r' || (c == '\n' && prev != '\r')) {
+        line++;
+      }
+      prev = c;
+    }
+    return new SourcePositionInfo(that.file, line, 0);
+  }
+
+  public static SourcePositionInfo findBeginning(SourcePositionInfo that, String str) {
+    if (that == null) {
+      return null;
+    }
+    int line = that.line - 1; // -1 because, well, it seems to work
+    int prev = 0;
+    for (int i = str.length() - 1; i >= 0; i--) {
+      char c = str.charAt(i);
+      if ((c == '\r' && prev != '\n') || (c == '\n')) {
+        line--;
+      }
+      prev = c;
+    }
+    return new SourcePositionInfo(that.file, line, 0);
+  }
+
+  @Override
+  public String toString() {
+    return file + ':' + line;
+  }
+
+  public int compareTo(Object o) {
+    SourcePositionInfo that = (SourcePositionInfo) o;
+    int r = this.file.compareTo(that.file);
+    if (r != 0) return r;
+    return this.line - that.line;
+  }
+  
+  /**
+   * Build a SourcePositionInfo from the XML source= notation
+   */
+  public static SourcePositionInfo fromXml(String source) {
+    if (source != null) {
+      for (int i = 0; i < source.length(); i++) {
+        if (source.charAt(i) == ':') {
+          return new SourcePositionInfo(source.substring(0, i), Integer.parseInt(source
+              .substring(i + 1)), 0);
+        }
+      }
+    }
+
+    return UNKNOWN;
+  }
+
+  public String file;
+  public int line;
+  public int column;
+}
diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java
new file mode 100644
index 0000000..1fb67e8
--- /dev/null
+++ b/src/com/google/doclava/Stubs.java
@@ -0,0 +1,948 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+
+public class Stubs {
+  private static HashSet<ClassInfo> notStrippable;
+
+  public static void writeStubsAndXml(String stubsDir, String xmlFile,
+      HashSet<String> stubPackages) {
+    // figure out which classes we need
+    notStrippable = new HashSet<ClassInfo>();
+    ClassInfo[] all = Converter.allClasses();
+    PrintStream xmlWriter = null;
+    if (xmlFile != null) {
+      try {
+        File xml = new File(xmlFile);
+        xml.getParentFile().mkdirs();
+        xmlWriter = new PrintStream(xml);
+      } catch (FileNotFoundException e) {
+        Errors.error(Errors.IO_ERROR, new SourcePositionInfo(xmlFile, 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) {
+      if (cl.checkLevel() && cl.isIncluded()) {
+        cantStripThis(cl, notStrippable, "0:0");
+      }
+    }
+
+    // complain about anything that looks includeable but is not supposed to
+    // be written, e.g. hidden things
+    for (ClassInfo cl : notStrippable) {
+      if (!cl.isHidden()) {
+        MethodInfo[] methods = cl.selfMethods();
+        for (MethodInfo m : methods) {
+          if (m.isHidden()) {
+            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Reference to hidden method "
+                + m.name());
+          } else if (m.isDeprecated()) {
+            // don't bother reporting deprecated methods
+            // unless they are public
+            Errors.error(Errors.DEPRECATED, m.position(), "Method " + cl.qualifiedName() + "."
+                + m.name() + " is deprecated");
+          }
+
+          ClassInfo returnClass = m.returnType().asClassInfo();
+          if (returnClass != null && returnClass.isHidden()) {
+            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Method " + cl.qualifiedName()
+                + "." + m.name() + " returns unavailable type " + returnClass.name());
+          }
+
+          ParameterInfo[] params = m.parameters();
+          for (ParameterInfo p : params) {
+            TypeInfo t = p.type();
+            if (!t.isPrimitive()) {
+              if (t.asClassInfo().isHidden()) {
+                Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Parameter of hidden type "
+                    + t.fullName() + " in " + cl.qualifiedName() + "." + m.name() + "()");
+              }
+            }
+          }
+        }
+
+        // annotations are handled like methods
+        methods = cl.annotationElements();
+        for (MethodInfo m : methods) {
+          if (m.isHidden()) {
+            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Reference to hidden annotation "
+                + m.name());
+          }
+
+          ClassInfo returnClass = m.returnType().asClassInfo();
+          if (returnClass != null && returnClass.isHidden()) {
+            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Annotation '" + m.name()
+                + "' returns unavailable type " + returnClass.name());
+          }
+
+          ParameterInfo[] params = m.parameters();
+          for (ParameterInfo p : params) {
+            TypeInfo t = p.type();
+            if (!t.isPrimitive()) {
+              if (t.asClassInfo().isHidden()) {
+                Errors.error(Errors.UNAVAILABLE_SYMBOL, p.position(),
+                    "Reference to unavailable annotation class " + t.fullName());
+              }
+            }
+          }
+        }
+      } else if (cl.isDeprecated()) {
+        // not hidden, but deprecated
+        Errors.error(Errors.DEPRECATED, cl.position(), "Class " + cl.qualifiedName()
+            + " is deprecated");
+      }
+    }
+
+    HashMap<PackageInfo, List<ClassInfo>> packages = new HashMap<PackageInfo, List<ClassInfo>>();
+    for (ClassInfo cl : notStrippable) {
+      if (!cl.isDocOnly()) {
+        if (stubPackages == null || stubPackages.contains(cl.containingPackage().name())) {
+          // write out the stubs
+          if (stubsDir != null) {
+            writeClassFile(stubsDir, cl);
+          }
+          // build class list for xml file
+          if (xmlWriter != null) {
+            if (packages.containsKey(cl.containingPackage())) {
+              packages.get(cl.containingPackage()).add(cl);
+            } else {
+              ArrayList<ClassInfo> classes = new ArrayList<ClassInfo>();
+              classes.add(cl);
+              packages.put(cl.containingPackage(), classes);
+            }
+          }
+        }
+      }
+    }
+
+    // write out the XML
+    if (xmlWriter != null) {
+      writeXML(xmlWriter, packages, notStrippable);
+      xmlWriter.close();
+    }
+  }
+
+  public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable, String why) {
+
+    if (!notStrippable.add(cl)) {
+      // slight optimization: if it already contains cl, it already contains
+      // all of cl's parents
+      return;
+    }
+    cl.setReasonIncluded(why);
+
+    // cant strip annotations
+    /*
+     * if (cl.annotations() != null){ for (AnnotationInstanceInfo ai : cl.annotations()){ if
+     * (ai.type() != null){ cantStripThis(ai.type(), notStrippable, "1:" + cl.qualifiedName()); } }
+     * }
+     */
+    // cant strip any public fields or their generics
+    if (cl.allSelfFields() != null) {
+      for (FieldInfo fInfo : cl.allSelfFields()) {
+        if (fInfo.type() != null) {
+          if (fInfo.type().asClassInfo() != null) {
+            cantStripThis(fInfo.type().asClassInfo(), notStrippable, "2:" + cl.qualifiedName());
+          }
+          if (fInfo.type().typeArguments() != null) {
+            for (TypeInfo tTypeInfo : fInfo.type().typeArguments()) {
+              if (tTypeInfo.asClassInfo() != null) {
+                cantStripThis(tTypeInfo.asClassInfo(), notStrippable, "3:" + cl.qualifiedName());
+              }
+            }
+          }
+        }
+      }
+    }
+    // cant strip any of the type's generics
+    if (cl.asTypeInfo() != null) {
+      if (cl.asTypeInfo().typeArguments() != null) {
+        for (TypeInfo tInfo : cl.asTypeInfo().typeArguments()) {
+          if (tInfo.asClassInfo() != null) {
+            cantStripThis(tInfo.asClassInfo(), notStrippable, "4:" + cl.qualifiedName());
+          }
+        }
+      }
+    }
+    // cant strip any of the annotation elements
+    // cantStripThis(cl.annotationElements(), notStrippable);
+    // take care of methods
+    cantStripThis(cl.allSelfMethods(), notStrippable);
+    cantStripThis(cl.allConstructors(), notStrippable);
+    // blow the outer class open if this is an inner class
+    if (cl.containingClass() != null) {
+      cantStripThis(cl.containingClass(), notStrippable, "5:" + cl.qualifiedName());
+    }
+    // blow open super class and interfaces
+    ClassInfo supr = cl.realSuperclass();
+    if (supr != null) {
+      if (supr.isHidden()) {
+        // cl is a public class declared as extending a hidden superclass.
+        // this is not a desired practice but it's happened, so we deal
+        // with it by stripping off the superclass relation for purposes of
+        // generating the doc & stub information, and proceeding normally.
+        cl.init(cl.asTypeInfo(), cl.realInterfaces(), cl.realInterfaceTypes(), cl.innerClasses(),
+            cl.allConstructors(), cl.allSelfMethods(), cl.annotationElements(), cl.allSelfFields(),
+            cl.enumConstants(), cl.containingPackage(), cl.containingClass(), null, null, cl
+                .annotations());
+        Errors.error(Errors.HIDDEN_SUPERCLASS, cl.position(), "Public class " + cl.qualifiedName()
+            + " stripped of unavailable superclass " + supr.qualifiedName());
+      } else {
+        cantStripThis(supr, notStrippable, "6:" + cl.realSuperclass().name() + cl.qualifiedName());
+      }
+    }
+  }
+
+  private static void cantStripThis(MethodInfo[] mInfos, HashSet<ClassInfo> notStrippable) {
+    // for each method, blow open the parameters, throws and return types. also blow open their
+    // generics
+    if (mInfos != null) {
+      for (MethodInfo mInfo : mInfos) {
+        if (mInfo.getTypeParameters() != null) {
+          for (TypeInfo tInfo : mInfo.getTypeParameters()) {
+            if (tInfo.asClassInfo() != null) {
+              cantStripThis(tInfo.asClassInfo(), notStrippable, "8:"
+                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+            }
+          }
+        }
+        if (mInfo.parameters() != null) {
+          for (ParameterInfo pInfo : mInfo.parameters()) {
+            if (pInfo.type() != null && pInfo.type().asClassInfo() != null) {
+              cantStripThis(pInfo.type().asClassInfo(), notStrippable, "9:"
+                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+              if (pInfo.type().typeArguments() != null) {
+                for (TypeInfo tInfoType : pInfo.type().typeArguments()) {
+                  if (tInfoType.asClassInfo() != null) {
+                    ClassInfo tcl = tInfoType.asClassInfo();
+                    if (tcl.isHidden()) {
+                      Errors
+                          .error(Errors.UNAVAILABLE_SYMBOL, mInfo.position(),
+                              "Parameter of hidden type " + tInfoType.fullName() + " in "
+                                  + mInfo.containingClass().qualifiedName() + '.' + mInfo.name()
+                                  + "()");
+                    } else {
+                      cantStripThis(tcl, notStrippable, "10:"
+                          + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+        for (ClassInfo thrown : mInfo.thrownExceptions()) {
+          cantStripThis(thrown, notStrippable, "11:" + mInfo.realContainingClass().qualifiedName()
+              + ":" + mInfo.name());
+        }
+        if (mInfo.returnType() != null && mInfo.returnType().asClassInfo() != null) {
+          cantStripThis(mInfo.returnType().asClassInfo(), notStrippable, "12:"
+              + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+          if (mInfo.returnType().typeArguments() != null) {
+            for (TypeInfo tyInfo : mInfo.returnType().typeArguments()) {
+              if (tyInfo.asClassInfo() != null) {
+                cantStripThis(tyInfo.asClassInfo(), notStrippable, "13:"
+                    + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  static String javaFileName(ClassInfo cl) {
+    String dir = "";
+    PackageInfo pkg = cl.containingPackage();
+    if (pkg != null) {
+      dir = pkg.name();
+      dir = dir.replace('.', '/') + '/';
+    }
+    return dir + cl.name() + ".java";
+  }
+
+  static void writeClassFile(String stubsDir, ClassInfo cl) {
+    // inner classes are written by their containing class
+    if (cl.containingClass() != null) {
+      return;
+    }
+
+    // Work around the bogus "Array" class we invent for
+    // Arrays.copyOf's Class<? extends T[]> newType parameter. (http://b/2715505)
+    if (cl.containingPackage() != null
+        && cl.containingPackage().name().equals(PackageInfo.DEFAULT_PACKAGE)) {
+      return;
+    }
+
+    String filename = stubsDir + '/' + javaFileName(cl);
+    File file = new File(filename);
+    ClearPage.ensureDirectory(file);
+
+    PrintStream stream = null;
+    try {
+      stream = new PrintStream(file);
+      writeClassFile(stream, cl);
+    } catch (FileNotFoundException e) {
+      System.err.println("error writing file: " + filename);
+    } finally {
+      if (stream != null) {
+        stream.close();
+      }
+    }
+  }
+
+  static void writeClassFile(PrintStream stream, ClassInfo cl) {
+    PackageInfo pkg = cl.containingPackage();
+    if (pkg != null) {
+      stream.println("package " + pkg.name() + ";");
+    }
+    writeClass(stream, cl);
+  }
+
+  static void writeClass(PrintStream stream, ClassInfo cl) {
+    writeAnnotations(stream, cl.annotations());
+
+    stream.print(cl.scope() + " ");
+    if (cl.isAbstract() && !cl.isAnnotation() && !cl.isInterface()) {
+      stream.print("abstract ");
+    }
+    if (cl.isStatic()) {
+      stream.print("static ");
+    }
+    if (cl.isFinal() && !cl.isEnum()) {
+      stream.print("final ");
+    }
+    if (false) {
+      stream.print("strictfp ");
+    }
+
+    HashSet<String> classDeclTypeVars = new HashSet();
+    String leafName = cl.asTypeInfo().fullName(classDeclTypeVars);
+    int bracket = leafName.indexOf('<');
+    if (bracket < 0) bracket = leafName.length() - 1;
+    int period = leafName.lastIndexOf('.', bracket);
+    if (period < 0) period = -1;
+    leafName = leafName.substring(period + 1);
+
+    String kind = cl.kind();
+    stream.println(kind + " " + leafName);
+
+    TypeInfo base = cl.superclassType();
+
+    if (!"enum".equals(kind)) {
+      if (base != null && !"java.lang.Object".equals(base.qualifiedTypeName())) {
+        stream.println("  extends " + base.fullName(classDeclTypeVars));
+      }
+    }
+
+    TypeInfo[] interfaces = cl.realInterfaceTypes();
+    List<TypeInfo> usedInterfaces = new ArrayList<TypeInfo>();
+    for (TypeInfo iface : interfaces) {
+      if (notStrippable.contains(iface.asClassInfo()) && !iface.asClassInfo().isDocOnly()) {
+        usedInterfaces.add(iface);
+      }
+    }
+    if (usedInterfaces.size() > 0 && !cl.isAnnotation()) {
+      // can java annotations extend other ones?
+      if (cl.isInterface() || cl.isAnnotation()) {
+        stream.print("  extends ");
+      } else {
+        stream.print("  implements ");
+      }
+      String comma = "";
+      for (TypeInfo iface : usedInterfaces) {
+        stream.print(comma + iface.fullName(classDeclTypeVars));
+        comma = ", ";
+      }
+      stream.println();
+    }
+
+    stream.println("{");
+
+    FieldInfo[] enumConstants = cl.enumConstants();
+    int N = enumConstants.length;
+    for (int i = 0; i < N; i++) {
+      FieldInfo field = enumConstants[i];
+      if (!field.constantLiteralValue().equals("null")) {
+        stream.println(field.name() + "(" + field.constantLiteralValue()
+            + (i == N - 1 ? ");" : "),"));
+      } else {
+        stream.println(field.name() + "(" + (i == N - 1 ? ");" : "),"));
+      }
+    }
+
+    for (ClassInfo inner : cl.getRealInnerClasses()) {
+      if (notStrippable.contains(inner) && !inner.isDocOnly()) {
+        writeClass(stream, inner);
+      }
+    }
+
+
+    for (MethodInfo method : cl.constructors()) {
+      if (!method.isDocOnly()) {
+        writeMethod(stream, method, true);
+      }
+    }
+
+    boolean fieldNeedsInitialization = false;
+    boolean staticFieldNeedsInitialization = false;
+    for (FieldInfo field : cl.allSelfFields()) {
+      if (!field.isDocOnly()) {
+        if (!field.isStatic() && field.isFinal() && !fieldIsInitialized(field)) {
+          fieldNeedsInitialization = true;
+        }
+        if (field.isStatic() && field.isFinal() && !fieldIsInitialized(field)) {
+          staticFieldNeedsInitialization = true;
+        }
+      }
+    }
+
+    // The compiler includes a default public constructor that calls the super classes
+    // default constructor in the case where there are no written constructors.
+    // So, if we hide all the constructors, java may put in a constructor
+    // that calls a nonexistent super class constructor. So, if there are no constructors,
+    // and the super class doesn't have a default constructor, write in a private constructor
+    // that works. TODO -- we generate this as protected, but we really should generate
+    // it as private unless it also exists in the real code.
+    if ((cl.constructors().length == 0 && (cl.getNonWrittenConstructors().length != 0 || fieldNeedsInitialization))
+        && !cl.isAnnotation() && !cl.isInterface() && !cl.isEnum()) {
+      // Errors.error(Errors.HIDDEN_CONSTRUCTOR,
+      // cl.position(), "No constructors " +
+      // "found and superclass has no parameterless constructor.  A constructor " +
+      // "that calls an appropriate superclass constructor " +
+      // "was automatically written to stubs.\n");
+      stream.println(cl.leafName() + "() { " + superCtorCall(cl, null) + "throw new"
+          + " RuntimeException(\"Stub!\"); }");
+    }
+
+    for (MethodInfo method : cl.allSelfMethods()) {
+      if (cl.isEnum()) {
+        if (("values".equals(method.name()) && "()".equals(method.signature()))
+            || ("valueOf".equals(method.name()) && "(java.lang.String)".equals(method.signature()))) {
+          // skip these two methods on enums, because they're synthetic,
+          // although for some reason javadoc doesn't mark them as synthetic,
+          // maybe because they still want them documented
+          continue;
+        }
+      }
+      if (!method.isDocOnly()) {
+        writeMethod(stream, method, false);
+      }
+    }
+    // Write all methods that are hidden, but override abstract methods or interface methods.
+    // These can't be hidden.
+    for (MethodInfo method : cl.getHiddenMethods()) {
+      MethodInfo overriddenMethod =
+          method.findRealOverriddenMethod(method.name(), method.signature(), notStrippable);
+      ClassInfo classContainingMethod =
+          method.findRealOverriddenClass(method.name(), method.signature());
+      if (overriddenMethod != null && !overriddenMethod.isHidden() && !overriddenMethod.isDocOnly()
+          && (overriddenMethod.isAbstract() || overriddenMethod.containingClass().isInterface())) {
+        method.setReason("1:" + classContainingMethod.qualifiedName());
+        cl.addMethod(method);
+        writeMethod(stream, method, false);
+      }
+    }
+
+    for (MethodInfo element : cl.annotationElements()) {
+      if (!element.isDocOnly()) {
+        writeAnnotationElement(stream, element);
+      }
+    }
+
+    for (FieldInfo field : cl.allSelfFields()) {
+      if (!field.isDocOnly()) {
+        writeField(stream, field);
+      }
+    }
+
+    if (staticFieldNeedsInitialization) {
+      stream.print("static { ");
+      for (FieldInfo field : cl.allSelfFields()) {
+        if (!field.isDocOnly() && field.isStatic() && field.isFinal() && !fieldIsInitialized(field)
+            && field.constantValue() == null) {
+          stream.print(field.name() + " = " + field.type().defaultValue() + "; ");
+        }
+      }
+      stream.println("}");
+    }
+
+    stream.println("}");
+  }
+
+
+  static void writeMethod(PrintStream stream, MethodInfo method, boolean isConstructor) {
+    String comma;
+
+    stream.print(method.scope() + " ");
+    if (method.isStatic()) {
+      stream.print("static ");
+    }
+    if (method.isFinal()) {
+      stream.print("final ");
+    }
+    if (method.isAbstract()) {
+      stream.print("abstract ");
+    }
+    if (method.isSynchronized()) {
+      stream.print("synchronized ");
+    }
+    if (method.isNative()) {
+      stream.print("native ");
+    }
+    if (false /* method.isStictFP() */) {
+      stream.print("strictfp ");
+    }
+
+    stream.print(method.typeArgumentsName(new HashSet()) + " ");
+
+    if (!isConstructor) {
+      stream.print(method.returnType().fullName(method.typeVariables()) + " ");
+    }
+    String n = method.name();
+    int pos = n.lastIndexOf('.');
+    if (pos >= 0) {
+      n = n.substring(pos + 1);
+    }
+    stream.print(n + "(");
+    comma = "";
+    int count = 1;
+    int size = method.parameters().length;
+    for (ParameterInfo param : method.parameters()) {
+      stream.print(comma + fullParameterTypeName(method, param.type(), count == size) + " "
+          + param.name());
+      comma = ", ";
+      count++;
+    }
+    stream.print(")");
+
+    comma = "";
+    if (method.thrownExceptions().length > 0) {
+      stream.print(" throws ");
+      for (ClassInfo thrown : method.thrownExceptions()) {
+        stream.print(comma + thrown.qualifiedName());
+        comma = ", ";
+      }
+    }
+    if (method.isAbstract() || method.isNative() || method.containingClass().isInterface()) {
+      stream.println(";");
+    } else {
+      stream.print(" { ");
+      if (isConstructor) {
+        stream.print(superCtorCall(method.containingClass(), method.thrownExceptions()));
+      }
+      stream.println("throw new RuntimeException(\"Stub!\"); }");
+    }
+  }
+
+  static void writeField(PrintStream stream, FieldInfo field) {
+    stream.print(field.scope() + " ");
+    if (field.isStatic()) {
+      stream.print("static ");
+    }
+    if (field.isFinal()) {
+      stream.print("final ");
+    }
+    if (field.isTransient()) {
+      stream.print("transient ");
+    }
+    if (field.isVolatile()) {
+      stream.print("volatile ");
+    }
+
+    stream.print(field.type().fullName());
+    stream.print(" ");
+    stream.print(field.name());
+
+    if (fieldIsInitialized(field)) {
+      stream.print(" = " + field.constantLiteralValue());
+    }
+
+    stream.println(";");
+  }
+
+  static boolean fieldIsInitialized(FieldInfo field) {
+    return (field.isFinal() && field.constantValue() != null)
+        || !field.type().dimension().equals("") || field.containingClass().isInterface();
+  }
+
+  // Returns 'true' if the method is an @Override of a visible parent
+  // method implementation, and thus does not affect the API.
+  static boolean methodIsOverride(MethodInfo mi) {
+    // Abstract/static/final methods are always listed in the API description
+    if (mi.isAbstract() || mi.isStatic() || mi.isFinal()) {
+      return false;
+    }
+
+    // Find any relevant ancestor declaration and inspect it
+    MethodInfo om = mi.findSuperclassImplementation(notStrippable);
+    if (om != null) {
+      // Visibility mismatch is an API change, so check for it
+      if (mi.mIsPrivate == om.mIsPrivate && mi.mIsPublic == om.mIsPublic
+          && mi.mIsProtected == om.mIsProtected) {
+        // Look only for overrides of an ancestor class implementation,
+        // not of e.g. an abstract or interface method declaration
+        if (!om.isAbstract()) {
+          // If the parent is hidden, we can't rely on it to provide
+          // the API
+          if (!om.isHidden()) {
+            // If the only "override" turns out to be in our own class
+            // (which sometimes happens in concrete subclasses of
+            // abstract base classes), it's not really an override
+            if (!mi.mContainingClass.equals(om.mContainingClass)) {
+              return true;
+            }
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  static boolean canCallMethod(ClassInfo from, MethodInfo m) {
+    if (m.isPublic() || m.isProtected()) {
+      return true;
+    }
+    if (m.isPackagePrivate()) {
+      String fromPkg = from.containingPackage().name();
+      String pkg = m.containingClass().containingPackage().name();
+      if (fromPkg.equals(pkg)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // call a constructor, any constructor on this class's superclass.
+  static String superCtorCall(ClassInfo cl, ClassInfo[] thrownExceptions) {
+    ClassInfo base = cl.realSuperclass();
+    if (base == null) {
+      return "";
+    }
+    HashSet<String> exceptionNames = new HashSet<String>();
+    if (thrownExceptions != null) {
+      for (ClassInfo thrown : thrownExceptions) {
+        exceptionNames.add(thrown.name());
+      }
+    }
+    MethodInfo[] ctors = base.constructors();
+    MethodInfo ctor = null;
+    // bad exception indicates that the exceptions thrown by the super constructor
+    // are incompatible with the constructor we're using for the sub class.
+    Boolean badException = false;
+    for (MethodInfo m : ctors) {
+      if (canCallMethod(cl, m)) {
+        if (m.thrownExceptions() != null) {
+          for (ClassInfo thrown : m.thrownExceptions()) {
+            if (!exceptionNames.contains(thrown.name())) {
+              badException = true;
+            }
+          }
+        }
+        if (badException) {
+          badException = false;
+          continue;
+        }
+        // if it has no args, we're done
+        if (m.parameters().length == 0) {
+          return "";
+        }
+        ctor = m;
+      }
+    }
+    if (ctor != null) {
+      String result = "";
+      result += "super(";
+      ParameterInfo[] params = ctor.parameters();
+      int N = params.length;
+      for (int i = 0; i < N; i++) {
+        TypeInfo t = params[i].type();
+        if (t.isPrimitive() && t.dimension().equals("")) {
+          String n = t.simpleTypeName();
+          if (("byte".equals(n) || "short".equals(n) || "int".equals(n) || "long".equals(n)
+              || "float".equals(n) || "double".equals(n))
+              && t.dimension().equals("")) {
+            result += "0";
+          } else if ("char".equals(n)) {
+            result += "'\\0'";
+          } else if ("boolean".equals(n)) {
+            result += "false";
+          } else {
+            result += "<<unknown-" + n + ">>";
+          }
+        } else {
+          // put null in each super class method. Cast null to the correct type
+          // to avoid collisions with other constructors. If the type is generic
+          // don't cast it
+          result +=
+              (!t.isTypeVariable() ? "(" + t.qualifiedTypeName() + t.dimension() + ")" : "")
+                  + "null";
+        }
+        if (i != N - 1) {
+          result += ",";
+        }
+      }
+      result += "); ";
+      return result;
+    } else {
+      return "";
+    }
+  }
+
+  static void writeAnnotations(PrintStream stream, AnnotationInstanceInfo[] annotations) {
+    for (AnnotationInstanceInfo ann : annotations) {
+      if (!ann.type().isHidden()) {
+        stream.println(ann.toString());
+      }
+    }
+  }
+
+  static void writeAnnotationElement(PrintStream stream, MethodInfo ann) {
+    stream.print(ann.returnType().fullName());
+    stream.print(" ");
+    stream.print(ann.name());
+    stream.print("()");
+    AnnotationValueInfo def = ann.defaultAnnotationElementValue();
+    if (def != null) {
+      stream.print(" default ");
+      stream.print(def.valueString());
+    }
+    stream.println(";");
+  }
+
+  static void writeXML(PrintStream xmlWriter, HashMap<PackageInfo, List<ClassInfo>> allClasses,
+      HashSet 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);
+
+    xmlWriter.println("<api>");
+    for (PackageInfo pack : allPackages) {
+      writePackageXML(xmlWriter, pack, allClasses.get(pack), notStrippable);
+    }
+    xmlWriter.println("</api>");
+  }
+
+  static void writePackageXML(PrintStream xmlWriter, PackageInfo pack, List<ClassInfo> classList,
+      HashSet notStrippable) {
+    ClassInfo[] classes = classList.toArray(new ClassInfo[classList.size()]);
+    Arrays.sort(classes, ClassInfo.comparator);
+    // 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;
+    }
+    xmlWriter.println("<package name=\"" + pack.name() + "\"\n"
+    // + " source=\"" + pack.position() + "\"\n"
+        + ">");
+    for (ClassInfo cl : classes) {
+      writeClassXML(xmlWriter, cl, notStrippable);
+    }
+    xmlWriter.println("</package>");
+
+
+  }
+
+  static void writeClassXML(PrintStream xmlWriter, ClassInfo cl, HashSet notStrippable) {
+    String scope = cl.scope();
+    String deprecatedString = "";
+    String declString = (cl.isInterface()) ? "interface" : "class";
+    if (cl.isDeprecated()) {
+      deprecatedString = "deprecated";
+    } else {
+      deprecatedString = "not deprecated";
+    }
+    xmlWriter.println("<" + declString + " name=\"" + cl.name() + "\"");
+    if (!cl.isInterface() && !cl.qualifiedName().equals("java.lang.Object")) {
+      xmlWriter.println(" extends=\""
+          + ((cl.realSuperclass() == null) ? "java.lang.Object" : cl.realSuperclass()
+              .qualifiedName()) + "\"");
+    }
+    xmlWriter.println(" abstract=\"" + cl.isAbstract() + "\"\n" + " static=\"" + cl.isStatic()
+        + "\"\n" + " final=\"" + cl.isFinal() + "\"\n" + " deprecated=\"" + deprecatedString
+        + "\"\n" + " visibility=\"" + scope + "\"\n"
+        // + " source=\"" + cl.position() + "\"\n"
+        + ">");
+
+    ClassInfo[] interfaces = cl.realInterfaces();
+    Arrays.sort(interfaces, ClassInfo.comparator);
+    for (ClassInfo iface : interfaces) {
+      if (notStrippable.contains(iface)) {
+        xmlWriter.println("<implements name=\"" + iface.qualifiedName() + "\">");
+        xmlWriter.println("</implements>");
+      }
+    }
+
+    MethodInfo[] constructors = cl.constructors();
+    Arrays.sort(constructors, MethodInfo.comparator);
+    for (MethodInfo mi : constructors) {
+      writeConstructorXML(xmlWriter, mi);
+    }
+
+    MethodInfo[] methods = cl.allSelfMethods();
+    Arrays.sort(methods, MethodInfo.comparator);
+    for (MethodInfo mi : methods) {
+      if (!methodIsOverride(mi)) {
+        writeMethodXML(xmlWriter, mi);
+      }
+    }
+
+    FieldInfo[] fields = cl.allSelfFields();
+    Arrays.sort(fields, FieldInfo.comparator);
+    for (FieldInfo fi : fields) {
+      writeFieldXML(xmlWriter, fi);
+    }
+    xmlWriter.println("</" + declString + ">");
+
+  }
+
+  static void writeMethodXML(PrintStream xmlWriter, MethodInfo mi) {
+    String scope = mi.scope();
+
+    String deprecatedString = "";
+    if (mi.isDeprecated()) {
+      deprecatedString = "deprecated";
+    } else {
+      deprecatedString = "not deprecated";
+    }
+    xmlWriter.println("<method name=\""
+        + mi.name()
+        + "\"\n"
+        + ((mi.returnType() != null) ? " return=\""
+            + makeXMLcompliant(fullParameterTypeName(mi, mi.returnType(), false)) + "\"\n" : "")
+        + " abstract=\"" + mi.isAbstract() + "\"\n" + " native=\"" + mi.isNative() + "\"\n"
+        + " synchronized=\"" + mi.isSynchronized() + "\"\n" + " static=\"" + mi.isStatic() + "\"\n"
+        + " final=\"" + mi.isFinal() + "\"\n" + " deprecated=\"" + deprecatedString + "\"\n"
+        + " visibility=\"" + scope + "\"\n"
+        // + " source=\"" + mi.position() + "\"\n"
+        + ">");
+
+    // write parameters in declaration order
+    int numParameters = mi.parameters().length;
+    int count = 0;
+    for (ParameterInfo pi : mi.parameters()) {
+      count++;
+      writeParameterXML(xmlWriter, mi, pi, count == numParameters);
+    }
+
+    // but write exceptions in canonicalized order
+    ClassInfo[] exceptions = mi.thrownExceptions();
+    Arrays.sort(exceptions, ClassInfo.comparator);
+    for (ClassInfo pi : exceptions) {
+      xmlWriter.println("<exception name=\"" + pi.name() + "\" type=\"" + pi.qualifiedName()
+          + "\">");
+      xmlWriter.println("</exception>");
+    }
+    xmlWriter.println("</method>");
+  }
+
+  static void writeConstructorXML(PrintStream xmlWriter, MethodInfo mi) {
+    String scope = mi.scope();
+    String deprecatedString = "";
+    if (mi.isDeprecated()) {
+      deprecatedString = "deprecated";
+    } else {
+      deprecatedString = "not deprecated";
+    }
+    xmlWriter.println("<constructor name=\"" + mi.name() + "\"\n" + " type=\""
+        + mi.containingClass().qualifiedName() + "\"\n" + " static=\"" + mi.isStatic() + "\"\n"
+        + " final=\"" + mi.isFinal() + "\"\n" + " deprecated=\"" + deprecatedString + "\"\n"
+        + " visibility=\"" + scope + "\"\n"
+        // + " source=\"" + mi.position() + "\"\n"
+        + ">");
+
+    int numParameters = mi.parameters().length;
+    int count = 0;
+    for (ParameterInfo pi : mi.parameters()) {
+      count++;
+      writeParameterXML(xmlWriter, mi, pi, count == numParameters);
+    }
+
+    ClassInfo[] exceptions = mi.thrownExceptions();
+    Arrays.sort(exceptions, ClassInfo.comparator);
+    for (ClassInfo pi : exceptions) {
+      xmlWriter.println("<exception name=\"" + pi.name() + "\" type=\"" + pi.qualifiedName()
+          + "\">");
+      xmlWriter.println("</exception>");
+    }
+    xmlWriter.println("</constructor>");
+  }
+
+  static void writeParameterXML(PrintStream xmlWriter, MethodInfo method, ParameterInfo pi,
+      boolean isLast) {
+    xmlWriter.println("<parameter name=\"" + pi.name() + "\" type=\""
+        + makeXMLcompliant(fullParameterTypeName(method, pi.type(), isLast)) + "\">");
+    xmlWriter.println("</parameter>");
+  }
+
+  static void writeFieldXML(PrintStream xmlWriter, FieldInfo fi) {
+    String scope = fi.scope();
+    String deprecatedString = "";
+    if (fi.isDeprecated()) {
+      deprecatedString = "deprecated";
+    } else {
+      deprecatedString = "not deprecated";
+    }
+    // need to make sure value is valid XML
+    String value = makeXMLcompliant(fi.constantLiteralValue());
+
+    String fullTypeName = makeXMLcompliant(fi.type().qualifiedTypeName()) + fi.type().dimension();
+
+    xmlWriter.println("<field name=\"" + fi.name() + "\"\n" + " type=\"" + fullTypeName + "\"\n"
+        + " transient=\"" + fi.isTransient() + "\"\n" + " volatile=\"" + fi.isVolatile() + "\"\n"
+        + (fieldIsInitialized(fi) ? " value=\"" + value + "\"\n" : "") + " static=\""
+        + fi.isStatic() + "\"\n" + " final=\"" + fi.isFinal() + "\"\n" + " deprecated=\""
+        + deprecatedString + "\"\n" + " visibility=\"" + scope + "\"\n"
+        // + " source=\"" + fi.position() + "\"\n"
+        + ">");
+    xmlWriter.println("</field>");
+  }
+
+  static String makeXMLcompliant(String s) {
+    String returnString = "";
+    returnString = s.replaceAll("&", "&amp;");
+    returnString = returnString.replaceAll("<", "&lt;");
+    returnString = returnString.replaceAll(">", "&gt;");
+    returnString = returnString.replaceAll("\"", "&quot;");
+    returnString = returnString.replaceAll("'", "&pos;");
+    return returnString;
+  }
+
+  static String fullParameterTypeName(MethodInfo method, TypeInfo type, boolean isLast) {
+    String fullTypeName = type.fullName(method.typeVariables());
+    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;
+  }
+}
diff --git a/src/com/google/doclava/TagInfo.java b/src/com/google/doclava/TagInfo.java
new file mode 100644
index 0000000..09d4e35
--- /dev/null
+++ b/src/com/google/doclava/TagInfo.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+public class TagInfo {
+  private String mName;
+  private String mText;
+  private String mKind;
+  private SourcePositionInfo mPosition;
+
+  TagInfo(String n, String k, String t, SourcePositionInfo sp) {
+    mName = n;
+    mText = t;
+    mKind = k;
+    mPosition = sp;
+  }
+
+  String name() {
+    return mName;
+  }
+
+  String text() {
+    return mText;
+  }
+
+  String kind() {
+    return mKind;
+  }
+
+  SourcePositionInfo position() {
+    return mPosition;
+  }
+
+  void setKind(String kind) {
+    mKind = kind;
+  }
+
+  public void makeHDF(Data data, String base) {
+    data.setValue(base + ".name", name());
+    data.setValue(base + ".text", text());
+    data.setValue(base + ".kind", kind());
+  }
+
+  public static void makeHDF(Data data, String base, TagInfo[] tags) {
+    makeHDF(data, base, tags, null, 0, 0);
+  }
+
+  public static void makeHDF(Data data, String base, InheritedTags tags) {
+    makeHDF(data, base, tags.tags(), tags.inherited(), 0, 0);
+  }
+
+  private static int makeHDF(Data data, String base, TagInfo[] tags, InheritedTags inherited,
+      int j, int depth) {
+    int i;
+    int len = tags.length;
+    if (len == 0 && inherited != null) {
+      j = makeHDF(data, base, inherited.tags(), inherited.inherited(), j, depth + 1);
+    } else {
+      for (i = 0; i < len; i++, j++) {
+        TagInfo t = tags[i];
+        if (inherited != null && t.name().equals("@inheritDoc")) {
+          j = makeHDF(data, base, inherited.tags(), inherited.inherited(), j, depth + 1);
+        } else {
+          if (t.name().equals("@inheritDoc")) {
+            Errors.error(Errors.BAD_INHERITDOC, t.mPosition,
+                "@inheritDoc on class/method that is not inherited");
+          }
+          t.makeHDF(data, base + "." + j);
+        }
+      }
+    }
+    return j;
+  }
+}
diff --git a/src/com/google/doclava/TextTagInfo.java b/src/com/google/doclava/TextTagInfo.java
new file mode 100644
index 0000000..35a486b
--- /dev/null
+++ b/src/com/google/doclava/TextTagInfo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+public class TextTagInfo extends TagInfo {
+  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
new file mode 100644
index 0000000..5f49485
--- /dev/null
+++ b/src/com/google/doclava/ThrowsTagInfo.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+public class ThrowsTagInfo extends ParsedTagInfo {
+  static final Pattern PATTERN = Pattern.compile("(\\S+)\\s+(.*)", Pattern.DOTALL);
+  private ClassInfo mException;
+
+  public ThrowsTagInfo(String name, String kind, String text, ContainerInfo base,
+      SourcePositionInfo sp) {
+    super(name, kind, text, base, sp);
+
+    Matcher m = PATTERN.matcher(text);
+    if (m.matches()) {
+      setCommentText(m.group(2));
+      String className = m.group(1);
+      if (base instanceof ClassInfo) {
+        mException = ((ClassInfo) base).findClass(className);
+      }
+      if (mException == null) {
+        mException = Converter.obtainClass(className);
+      }
+    }
+  }
+
+  public ThrowsTagInfo(String name, String kind, String text, ClassInfo exception,
+      String exceptionComment, ContainerInfo base, SourcePositionInfo sp) {
+    super(name, kind, text, base, sp);
+    mException = exception;
+    setCommentText(exceptionComment);
+  }
+
+  public ClassInfo exception() {
+    return mException;
+  }
+
+  public TypeInfo exceptionType() {
+    if (mException != null) {
+      return mException.asTypeInfo();
+    } else {
+      return null;
+    }
+  }
+
+  public static void makeHDF(Data data, String base, ThrowsTagInfo[] tags) {
+    for (int i = 0; i < tags.length; i++) {
+      TagInfo.makeHDF(data, base + '.' + i + ".comment", tags[i].commentTags());
+      if (tags[i].exceptionType() != null) {
+        tags[i].exceptionType().makeHDF(data, base + "." + i + ".type");
+      }
+    }
+  }
+
+
+}
diff --git a/src/com/google/doclava/TodoFile.java b/src/com/google/doclava/TodoFile.java
new file mode 100644
index 0000000..5cf4f1e
--- /dev/null
+++ b/src/com/google/doclava/TodoFile.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.*;
+
+public class TodoFile {
+
+  public static final String MISSING = "No description text";
+
+  public static boolean areTagsUseful(InheritedTags tags) {
+    while (tags != null) {
+      if (areTagsUseful(tags.tags())) {
+        return true;
+      }
+      tags = tags.inherited();
+    }
+    return false;
+  }
+
+  public static boolean areTagsUseful(TagInfo[] tags) {
+    for (TagInfo t : tags) {
+      if ("Text".equals(t.name()) && t.text().trim().length() != 0) {
+        return true;
+      }
+      if ("@inheritDoc".equals(t.name())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static void setHDF(Data data, String base, SourcePositionInfo pos, String name,
+      String descr) {
+    data.setValue(base + ".pos", pos.toString());
+    data.setValue(base + ".name", name);
+    data.setValue(base + ".descr", descr);
+  }
+
+  static class PackageStats {
+    String name;
+    public int total;
+    public int errors;
+  }
+
+  public static String percent(int a, int b) {
+    return "" + Math.round((((b - a) / (float) b)) * 100) + "%";
+  }
+
+  public static void writeTodoFile(String filename) {
+    Data data = Doclava.makeHDF();
+    Doclava.setPageTitle(data, "Missing Documentation");
+    TreeMap<String, PackageStats> packageStats = new TreeMap<String, PackageStats>();
+
+    ClassInfo[] classes = Converter.rootClasses();
+    Arrays.sort(classes);
+
+    int classIndex = 0;
+
+    for (ClassInfo cl : classes) {
+      if (cl.isHidden()) {
+        continue;
+      }
+
+      String classBase = "classes." + classIndex;
+
+      String base = classBase + ".errors.";
+      int errors = 0;
+      int total = 1;
+
+      if (!areTagsUseful(cl.inlineTags())) {
+        setHDF(data, base + errors, cl.position(), "&lt;class comment&gt;", MISSING);
+        errors++;
+      }
+
+
+      for (MethodInfo m : cl.constructors()) {
+        boolean good = true;
+        total++;
+        if (m.checkLevel()) {
+          if (!areTagsUseful(m.inlineTags())) {
+            setHDF(data, base + errors, m.position(), m.prettySignature(), MISSING);
+            good = false;
+          }
+        }
+        if (!good) {
+          errors++;
+        }
+      }
+
+      for (MethodInfo m : cl.selfMethods()) {
+        boolean good = true;
+        total++;
+        if (m.checkLevel()) {
+          if (!areTagsUseful(m.inlineTags())) {
+            setHDF(data, base + errors, m.position(), m.name() + m.prettySignature(), MISSING);
+            good = false;
+          }
+        }
+        if (!good) {
+          errors++;
+        }
+      }
+
+
+      for (FieldInfo f : cl.enumConstants()) {
+        boolean good = true;
+        total++;
+        if (f.checkLevel()) {
+          if (!areTagsUseful(f.inlineTags())) {
+            setHDF(data, base + errors, f.position(), f.name(), MISSING);
+            good = false;
+          }
+        }
+        if (!good) {
+          errors++;
+        }
+      }
+
+      for (FieldInfo f : cl.selfFields()) {
+        boolean good = true;
+        total++;
+        if (f.checkLevel()) {
+          if (!areTagsUseful(f.inlineTags())) {
+            setHDF(data, base + errors, f.position(), f.name(), MISSING);
+            good = false;
+          }
+        }
+        if (!good) {
+          errors++;
+        }
+      }
+
+      if (errors > 0) {
+        data.setValue(classBase + ".qualified", cl.qualifiedName());
+        data.setValue(classBase + ".errorCount", "" + errors);
+        data.setValue(classBase + ".totalCount", "" + total);
+        data.setValue(classBase + ".percentGood", percent(errors, total));
+      }
+
+      PackageInfo pkg = cl.containingPackage();
+      String pkgName = pkg != null ? pkg.name() : "";
+      PackageStats ps = packageStats.get(pkgName);
+      if (ps == null) {
+        ps = new PackageStats();
+        ps.name = pkgName;
+        packageStats.put(pkgName, ps);
+      }
+      ps.total += total;
+      ps.errors += errors;
+
+      classIndex++;
+    }
+
+    int allTotal = 0;
+    int allErrors = 0;
+
+    int i = 0;
+    for (PackageStats ps : packageStats.values()) {
+      data.setValue("packages." + i + ".name", "" + ps.name);
+      data.setValue("packages." + i + ".errorCount", "" + ps.errors);
+      data.setValue("packages." + i + ".totalCount", "" + ps.total);
+      data.setValue("packages." + i + ".percentGood", percent(ps.errors, ps.total));
+
+      allTotal += ps.total;
+      allErrors += ps.errors;
+
+      i++;
+    }
+
+    data.setValue("all.errorCount", "" + allErrors);
+    data.setValue("all.totalCount", "" + allTotal);
+    data.setValue("all.percentGood", percent(allErrors, allTotal));
+
+    ClearPage.write(data, "todo.cs", filename, true);
+  }
+}
diff --git a/src/com/google/doclava/TypeInfo.java b/src/com/google/doclava/TypeInfo.java
new file mode 100644
index 0000000..8572f59
--- /dev/null
+++ b/src/com/google/doclava/TypeInfo.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+import java.util.*;
+
+public class TypeInfo {
+  public static final Set<String> PRIMITIVE_TYPES = Collections.unmodifiableSet(
+      new HashSet<String>(Arrays.asList("boolean", "byte", "char", "double", "float", "int",
+      "long", "short", "void")));
+  
+  public TypeInfo(boolean isPrimitive, String dimension, String simpleTypeName,
+      String qualifiedTypeName, ClassInfo cl) {
+    mIsPrimitive = isPrimitive;
+    mDimension = dimension;
+    mSimpleTypeName = simpleTypeName;
+    mQualifiedTypeName = qualifiedTypeName;
+    mClass = cl;
+  }
+
+  public TypeInfo(String typeString) {
+    // VarArgs
+    if (typeString.endsWith("...")) {
+      typeString = typeString.substring(0, typeString.length() - 3);
+    }
+    
+    // Generic parameters
+    int paramStartPos = typeString.indexOf('<');
+    if (paramStartPos > -1) {
+      ArrayList<TypeInfo> generics = new ArrayList<TypeInfo>();
+      int paramEndPos = typeString.lastIndexOf('>');
+      
+      int entryStartPos = paramStartPos + 1;
+      int bracketNesting = 0;
+      for (int i = entryStartPos; i < paramEndPos; i++) {
+        char c = typeString.charAt(i);
+        if (c == ',' && bracketNesting == 0) {
+          String entry = typeString.substring(entryStartPos, i).trim();
+          TypeInfo info = new TypeInfo(entry);
+          generics.add(info);
+          entryStartPos = i + 1;
+        } else if (c == '<') {
+          bracketNesting++;
+        } else if (c == '>') {
+          bracketNesting--;
+        }
+      }
+     
+      TypeInfo info = new TypeInfo(typeString.substring(entryStartPos, paramEndPos).trim());
+      generics.add(info);
+      
+      mTypeArguments = new TypeInfo[generics.size()];
+      generics.toArray(mTypeArguments);
+      
+      if (paramEndPos < typeString.length() - 1) {
+        typeString = typeString.substring(0,paramStartPos) + typeString.substring(paramEndPos + 1);
+      } else {
+        typeString = typeString.substring(0,paramStartPos);
+      }
+    }
+    
+    // Dimensions
+    int pos = typeString.indexOf('['); 
+    if (pos > -1) {
+      mDimension = typeString.substring(pos);
+      typeString = typeString.substring(0, pos);
+    } else {
+      mDimension = "";
+    }
+   
+    if (PRIMITIVE_TYPES.contains(typeString)) {
+      mIsPrimitive = true;
+      mSimpleTypeName = typeString;
+      mQualifiedTypeName = typeString;
+    } else {
+      mQualifiedTypeName = typeString;
+      pos = typeString.lastIndexOf('.');
+      if (pos > -1) {
+        mSimpleTypeName = typeString.substring(pos + 1);
+      } else {
+        mSimpleTypeName = typeString;
+      }
+    }
+  }
+
+  public ClassInfo asClassInfo() {
+    return mClass;
+  }
+
+  public boolean isPrimitive() {
+    return mIsPrimitive;
+  }
+
+  public String dimension() {
+    return mDimension;
+  }
+
+  public String simpleTypeName() {
+    return mSimpleTypeName;
+  }
+
+  public String qualifiedTypeName() {
+    return mQualifiedTypeName;
+  }
+
+  public String fullName() {
+    if (mFullName != null) {
+      return mFullName;
+    } else {
+      return fullName(new HashSet<String>());
+    }
+  }
+
+  public static String typeArgumentsName(TypeInfo[] args, HashSet<String> typeVars) {
+    String result = "<";
+    for (int i = 0; i < args.length; i++) {
+      result += args[i].fullName(typeVars);
+      if (i != args.length - 1) {
+        result += ", ";
+      }
+    }
+    result += ">";
+    return result;
+  }
+
+  public String fullName(HashSet<String> typeVars) {
+    mFullName = fullNameNoDimension(typeVars) + mDimension;
+    return mFullName;
+  }
+
+  public String fullNameNoDimension(HashSet<String> typeVars) {
+    String fullName = null;
+    if (mIsTypeVariable) {
+      if (typeVars.contains(mQualifiedTypeName)) {
+        // don't recurse forever with the parameters. This handles
+        // Enum<K extends Enum<K>>
+        return mQualifiedTypeName;
+      }
+      typeVars.add(mQualifiedTypeName);
+    }
+    /*
+     * if (fullName != null) { return fullName; }
+     */
+    fullName = mQualifiedTypeName;
+    if (mTypeArguments != null && mTypeArguments.length > 0) {
+      fullName += typeArgumentsName(mTypeArguments, typeVars);
+    } else if (mSuperBounds != null && mSuperBounds.length > 0) {
+      fullName += " super " + mSuperBounds[0].fullName(typeVars);
+      for (int i = 1; i < mSuperBounds.length; i++) {
+        fullName += " & " + mSuperBounds[i].fullName(typeVars);
+      }
+    } else if (mExtendsBounds != null && mExtendsBounds.length > 0) {
+      fullName += " extends " + mExtendsBounds[0].fullName(typeVars);
+      for (int i = 1; i < mExtendsBounds.length; i++) {
+        fullName += " & " + mExtendsBounds[i].fullName(typeVars);
+      }
+    }
+    return fullName;
+  }
+
+  public TypeInfo[] typeArguments() {
+    return mTypeArguments;
+  }
+
+  public void makeHDF(Data data, String base) {
+    makeHDFRecursive(data, base, false, false, new HashSet<String>());
+  }
+
+  public void makeQualifiedHDF(Data data, String base) {
+    makeHDFRecursive(data, base, true, false, new HashSet<String>());
+  }
+
+  public void makeHDF(Data data, String base, boolean isLastVararg, HashSet<String> typeVariables) {
+    makeHDFRecursive(data, base, false, isLastVararg, typeVariables);
+  }
+
+  public void makeQualifiedHDF(Data data, String base, HashSet<String> typeVariables) {
+    makeHDFRecursive(data, base, true, false, typeVariables);
+  }
+
+  private void makeHDFRecursive(Data data, String base, boolean qualified, boolean isLastVararg,
+      HashSet<String> typeVars) {
+    String label = qualified ? qualifiedTypeName() : simpleTypeName();
+    label += (isLastVararg) ? "..." : dimension();
+    data.setValue(base + ".label", label);
+    if (mIsTypeVariable || mIsWildcard) {
+      // could link to an @param tag on the class to describe this
+      // but for now, just don't make it a link
+    } else if (!isPrimitive() && mClass != null) {
+      if (mClass.isIncluded()) {
+        data.setValue(base + ".link", mClass.htmlPage());
+        data.setValue(base + ".since", mClass.getSince());
+      } else {
+        Doclava.federationTagger.tagAll(new ClassInfo[] {mClass});
+        if (!mClass.getFederatedReferences().isEmpty()) {
+          FederatedSite site = mClass.getFederatedReferences().iterator().next();
+          data.setValue(base + ".link", site.linkFor(mClass.htmlPage()));
+          data.setValue(base + ".federated", site.name());
+        }
+      }
+    }
+
+    if (mIsTypeVariable) {
+      if (typeVars.contains(qualifiedTypeName())) {
+        // don't recurse forever with the parameters. This handles
+        // Enum<K extends Enum<K>>
+        return;
+      }
+      typeVars.add(qualifiedTypeName());
+    }
+    if (mTypeArguments != null) {
+      TypeInfo.makeHDF(data, base + ".typeArguments", mTypeArguments, qualified, typeVars);
+    }
+    if (mSuperBounds != null) {
+      TypeInfo.makeHDF(data, base + ".superBounds", mSuperBounds, qualified, typeVars);
+    }
+    if (mExtendsBounds != null) {
+      TypeInfo.makeHDF(data, base + ".extendsBounds", mExtendsBounds, qualified, typeVars);
+    }
+  }
+
+  public static void makeHDF(Data data, String base, TypeInfo[] types, boolean qualified,
+      HashSet<String> typeVariables) {
+    final int N = types.length;
+    for (int i = 0; i < N; i++) {
+      types[i].makeHDFRecursive(data, base + "." + i, qualified, false, typeVariables);
+    }
+  }
+
+  public static void makeHDF(Data data, String base, TypeInfo[] types, boolean qualified) {
+    makeHDF(data, base, types, qualified, new HashSet<String>());
+  }
+
+  void setTypeArguments(TypeInfo[] args) {
+    mTypeArguments = args;
+  }
+
+  void setBounds(TypeInfo[] superBounds, TypeInfo[] extendsBounds) {
+    mSuperBounds = superBounds;
+    mExtendsBounds = extendsBounds;
+  }
+
+  void setIsTypeVariable(boolean b) {
+    mIsTypeVariable = b;
+  }
+
+  void setIsWildcard(boolean b) {
+    mIsWildcard = b;
+  }
+
+  static HashSet<String> typeVariables(TypeInfo[] params) {
+    return typeVariables(params, new HashSet());
+  }
+
+  static HashSet<String> typeVariables(TypeInfo[] params, HashSet<String> result) {
+    for (TypeInfo t : params) {
+      if (t.mIsTypeVariable) {
+        result.add(t.mQualifiedTypeName);
+      }
+    }
+    return result;
+  }
+
+
+  public boolean isTypeVariable() {
+    return mIsTypeVariable;
+  }
+
+  public String defaultValue() {
+    if (mIsPrimitive) {
+      if ("boolean".equals(mSimpleTypeName)) {
+        return "false";
+      } else {
+        return "0";
+      }
+    } else {
+      return "null";
+    }
+  }
+
+  @Override
+  public String toString() {
+    String returnString = "";
+    returnString +=
+        "Primitive?: " + mIsPrimitive + " TypeVariable?: " + mIsTypeVariable + " Wildcard?: "
+            + mIsWildcard + " Dimension: " + mDimension + " QualifedTypeName: "
+            + mQualifiedTypeName;
+
+    if (mTypeArguments != null) {
+      returnString += "\nTypeArguments: ";
+      for (TypeInfo tA : mTypeArguments) {
+        returnString += tA.qualifiedTypeName() + "(" + tA + ") ";
+      }
+    }
+    if (mSuperBounds != null) {
+      returnString += "\nSuperBounds: ";
+      for (TypeInfo tA : mSuperBounds) {
+        returnString += tA.qualifiedTypeName() + "(" + tA + ") ";
+      }
+    }
+    if (mExtendsBounds != null) {
+      returnString += "\nExtendsBounds: ";
+      for (TypeInfo tA : mExtendsBounds) {
+        returnString += tA.qualifiedTypeName() + "(" + tA + ") ";
+      }
+    }
+    return returnString;
+  }
+
+  private boolean mIsPrimitive;
+  private boolean mIsTypeVariable;
+  private boolean mIsWildcard;
+  private String mDimension;
+  private String mSimpleTypeName;
+  private String mQualifiedTypeName;
+  private ClassInfo mClass;
+  private TypeInfo[] mTypeArguments;
+  private TypeInfo[] mSuperBounds;
+  private TypeInfo[] mExtendsBounds;
+  private String mFullName;
+}
diff --git a/src/com/google/doclava/apicheck/AbstractMethodInfo.java b/src/com/google/doclava/apicheck/AbstractMethodInfo.java
new file mode 100644
index 0000000..306dba8
--- /dev/null
+++ b/src/com/google/doclava/apicheck/AbstractMethodInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava.apicheck;
+
+public interface AbstractMethodInfo {
+
+  public void addException(String exec);
+
+  public void addParameter(com.google.doclava.ParameterInfo p);
+
+  public void setDeprecated(boolean deprecated);
+  
+  public void setVarargs(boolean varargs);
+  public boolean isVarArgs();
+}
diff --git a/src/com/google/doclava/apicheck/ApiCheck.java b/src/com/google/doclava/apicheck/ApiCheck.java
new file mode 100644
index 0000000..0ec19c1
--- /dev/null
+++ b/src/com/google/doclava/apicheck/ApiCheck.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava.apicheck;
+
+import com.google.doclava.AnnotationInstanceInfo;
+import com.google.doclava.ClassInfo;
+import com.google.doclava.ConstructorInfo;
+import com.google.doclava.Converter;
+import com.google.doclava.Errors;
+import com.google.doclava.MethodInfo;
+import com.google.doclava.PackageInfo;
+import com.google.doclava.ParameterInfo;
+import com.google.doclava.SourcePositionInfo;
+import com.google.doclava.TypeInfo;
+
+import com.google.doclava.FieldInfo;
+import com.google.doclava.Errors.ErrorMessage;
+
+import com.sun.javadoc.ClassDoc;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.Stack;
+
+public class ApiCheck {
+  // parse out and consume the -whatever command line flags
+  private static ArrayList<String[]> parseFlags(ArrayList<String> allArgs) {
+    ArrayList<String[]> ret = new ArrayList<String[]>();
+
+    int i;
+    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")) {
+        String[] arg = new String[2];
+        arg[0] = flag;
+        arg[1] = allArgs.get(++i);
+        ret.add(arg);
+      } else {
+        // we've consumed all of the -whatever args, so we're done
+        break;
+      }
+    }
+
+    // i now points to the first non-flag arg; strip what came before
+    for (; i > 0; i--) {
+      allArgs.remove(0);
+    }
+    return ret;
+  }
+
+  public static void main(String[] originalArgs) {
+    ApiCheck acheck = new ApiCheck();
+    Report report = acheck.checkApi(originalArgs);
+   
+    Errors.printErrors(report.errors());
+    System.exit(report.code);
+  }
+  
+  /**
+   * Compares two api xml files for consistency.
+   */
+  public Report checkApi(String[] originalArgs) {
+    // translate to an ArrayList<String> for munging
+    ArrayList<String> args = new ArrayList<String>(originalArgs.length);
+    for (String a : originalArgs) {
+      args.add(a);
+    }
+
+    ArrayList<String[]> flags = ApiCheck.parseFlags(args);
+    for (String[] a : flags) {
+      if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) {
+        try {
+          int level = -1;
+          if (a[0].equals("-error")) {
+            level = Errors.ERROR;
+          } else if (a[0].equals("-warning")) {
+            level = Errors.WARNING;
+          } else if (a[0].equals("-hide")) {
+            level = Errors.HIDDEN;
+          }
+          Errors.setErrorLevel(Integer.parseInt(a[1]), level);
+        } catch (NumberFormatException e) {
+          System.err.println("Bad argument: " + a[0] + " " + a[1]);
+          return new Report(2, Errors.getErrors());
+        }
+      }
+    }
+
+    ApiInfo oldApi;
+    ApiInfo newApi;
+    
+    try {
+      oldApi = parseApi(args.get(0));
+      newApi = parseApi(args.get(1));
+    } catch (ApiParseException e) {
+      e.printStackTrace();
+      System.err.println("Error parsing API");
+      return new Report(1, Errors.getErrors());
+    }
+
+    // only run the consistency check if we haven't had XML parse errors
+    if (!Errors.hadError) {
+      oldApi.isConsistent(newApi);
+    }
+
+    return new Report(Errors.hadError ? 1 : 0, Errors.getErrors());
+  }
+
+  public ApiInfo parseApi(String xmlFile) throws ApiParseException {
+    FileInputStream fileStream = null;
+    try {
+      fileStream = new FileInputStream(xmlFile);
+      return parseApi(fileStream);
+    } catch (IOException e) {
+      throw new ApiParseException("Could not open file for parsing: " + xmlFile, e);
+    } finally {
+      if (fileStream != null) {
+        try {
+          fileStream.close();
+        } catch (IOException ignored) {}
+      }
+    }
+  }
+  
+  public ApiInfo parseApi(URL xmlURL) throws ApiParseException {
+    InputStream xmlStream = null;
+    try {
+      xmlStream = xmlURL.openStream();
+      return parseApi(xmlStream);
+    } catch (IOException e) {
+      throw new ApiParseException("Could not open stream for parsing: " + xmlURL,e);
+    } finally {
+      if (xmlStream != null) {
+        try {
+          xmlStream.close();
+        } catch (IOException ignored) {}
+      }
+    }
+  }
+  
+  public ApiInfo parseApi(InputStream xmlStream) throws ApiParseException {
+    try {
+      XMLReader xmlreader = XMLReaderFactory.createXMLReader();
+      MakeHandler handler = new MakeHandler();
+      xmlreader.setContentHandler(handler);
+      xmlreader.setErrorHandler(handler);
+      xmlreader.parse(new InputSource(xmlStream));
+      ApiInfo apiInfo = handler.getApi();
+      apiInfo.resolveSuperclasses();
+      apiInfo.resolveInterfaces();
+      return apiInfo;
+    } catch (Exception e) {
+      throw new ApiParseException("Error parsing API", e);
+    }
+  }
+  
+  private class MakeHandler extends DefaultHandler {
+
+    private ApiInfo mApi;
+    private PackageInfo mCurrentPackage;
+    private ClassInfo mCurrentClass;
+    private AbstractMethodInfo mCurrentMethod;
+    private Stack<ClassInfo> mClassScope = new Stack<ClassInfo>();
+    
+
+    public MakeHandler() {
+      super();
+      mApi = new ApiInfo();
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) {
+      if (qName.equals("package")) {
+        mCurrentPackage =
+            new PackageInfo(attributes.getValue("name"), SourcePositionInfo.fromXml(attributes
+                .getValue("source")));
+      } else if (qName.equals("class") || qName.equals("interface")) {
+        // push the old outer scope for later recovery, then set
+        // up the new current class object
+        mClassScope.push(mCurrentClass);
+        
+        ClassDoc classDoc = null;
+        String rawCommentText = "";
+        SourcePositionInfo position = SourcePositionInfo.fromXml(attributes.getValue("source"));
+        String visibility = attributes.getValue("visibility");
+        boolean isPublic = "public".equals(visibility);
+        boolean isProtected = "protected".equals(visibility);
+        boolean isPrivate = "private".equals(visibility); 
+        boolean isPackagePrivate = !isPublic && !isPrivate && !isProtected;
+        boolean isStatic = Boolean.valueOf(attributes.getValue("static"));
+        boolean isInterface = qName.equals("interface");
+        boolean isAbstract = Boolean.valueOf(attributes.getValue("abstract"));
+        boolean isOrdinaryClass = qName.equals("class");
+        boolean isException = false; // TODO: check hierarchy for java.lang.Exception
+        boolean isError = false; // TODO: not sure.
+        boolean isEnum = false; // TODO: not sure.
+        boolean isAnnotation = false; // TODO: not sure.
+        boolean isFinal = Boolean.valueOf(attributes.getValue("final"));
+        boolean isIncluded = false;
+        String name = attributes.getValue("name");
+        String qualifiedName = qualifiedName(mCurrentPackage.name(), name, mCurrentClass);
+        String qualifiedTypeName = null; // TODO: not sure
+        boolean isPrimitive = false;
+        
+        mCurrentClass =
+            new ClassInfo(classDoc, rawCommentText, position, isPublic, isProtected, 
+            isPackagePrivate, isPrivate, isStatic, isInterface, isAbstract, isOrdinaryClass, 
+            isException, isError, isEnum, isAnnotation, isFinal, isIncluded, name, qualifiedName,
+            qualifiedTypeName, isPrimitive);
+        
+        mCurrentClass.setContainingPackage(mCurrentPackage);
+        String superclass = attributes.getValue("extends");
+        if (superclass == null && !isInterface && !"java.lang.Object".equals(qualifiedName)) {
+          throw new AssertionError("no superclass known for class " + name);
+        }
+        
+        // Resolve superclass after .xml completely parsed.
+        mApi.mapClassToSuper(mCurrentClass, superclass);
+        
+        TypeInfo typeInfo = Converter.obtainTypeFromString(qualifiedName) ;
+        mCurrentClass.setTypeInfo(typeInfo);
+        mCurrentClass.setAnnotations(new AnnotationInstanceInfo[] {});
+      } else if (qName.equals("method")) {
+        String rawCommentText = "";
+        TypeInfo[] typeParameters = new TypeInfo[0];
+        String name = attributes.getValue("name");
+        String signature = null; // TODO
+        ClassInfo containingClass = mCurrentClass;
+        ClassInfo realContainingClass = mCurrentClass;
+        String visibility = attributes.getValue("visibility");
+        boolean isPublic = "public".equals(visibility);
+        boolean isProtected = "protected".equals(visibility);
+        boolean isPrivate = "private".equals(visibility); 
+        boolean isPackagePrivate = !isPublic && !isPrivate && !isProtected;
+        boolean isFinal = Boolean.valueOf(attributes.getValue("final"));
+        boolean isStatic = Boolean.valueOf(attributes.getValue("static"));
+        boolean isSynthetic = false; // TODO
+        boolean isAbstract = Boolean.valueOf(attributes.getValue("abstract"));
+        boolean isSynchronized = Boolean.valueOf(attributes.getValue("synchronized"));
+        boolean isNative = Boolean.valueOf(attributes.getValue("native"));
+        boolean isAnnotationElement = false; // TODO
+        String kind = qName;
+        String flatSignature = null; // TODO
+        MethodInfo overriddenMethod = null; // TODO
+        TypeInfo returnType = Converter.obtainTypeFromString(attributes.getValue("return"));
+        ParameterInfo[] parameters = new ParameterInfo[0];
+        ClassInfo[] thrownExceptions = new ClassInfo[0];
+        SourcePositionInfo position = SourcePositionInfo.fromXml(attributes.getValue("source"));
+        AnnotationInstanceInfo[] annotations = new AnnotationInstanceInfo[] {}; // TODO
+        
+        mCurrentMethod = 
+            new MethodInfo(rawCommentText, typeParameters, name, signature, containingClass,
+            realContainingClass, isPublic, isProtected, isPackagePrivate, isPrivate, isFinal,
+            isStatic, isSynthetic, isAbstract, isSynchronized, isNative, isAnnotationElement, kind,
+            flatSignature, overriddenMethod, returnType, parameters, thrownExceptions, position,
+            annotations);
+        
+        mCurrentMethod.setDeprecated("deprecated".equals(attributes.getValue("deprecated")));
+      } else if (qName.equals("constructor")) {
+        mCurrentMethod =
+            new ConstructorInfo(attributes.getValue("name"), attributes.getValue("type"), Boolean
+                .valueOf(attributes.getValue("static")), Boolean.valueOf(attributes
+                .getValue("final")), attributes.getValue("deprecated"), attributes
+                .getValue("visibility"), SourcePositionInfo.fromXml(attributes.getValue("source")),
+                mCurrentClass);
+      } else if (qName.equals("field")) {
+        String visibility = attributes.getValue("visibility");
+        boolean isPublic = visibility.equals("public");
+        boolean isProtected = visibility.equals("protected");
+        boolean isPrivate = visibility.equals("private");
+        boolean isPackagePrivate = visibility.equals("");
+        String typeName = attributes.getValue("type");
+        TypeInfo type = Converter.obtainTypeFromString(typeName);
+        
+        FieldInfo fInfo =
+            new FieldInfo(attributes.getValue("name"), mCurrentClass, mCurrentClass, isPublic,
+            isProtected, isPackagePrivate, isPrivate, Boolean.valueOf(attributes.getValue("final")),
+            Boolean.valueOf(attributes.getValue("static")), Boolean.valueOf(attributes.
+            getValue("transient")), Boolean.valueOf(attributes.getValue("volatile")), false,
+            type, "", attributes.getValue("value"), SourcePositionInfo
+            .fromXml(attributes.getValue("source")), new AnnotationInstanceInfo[] {});
+        
+        fInfo.setDeprecated("deprecated".equals(attributes.getValue("deprecated")));
+        mCurrentClass.addField(fInfo);
+      } else if (qName.equals("parameter")) {
+        String name = attributes.getValue("name");
+        String typeName = attributes.getValue("type");
+        TypeInfo type = Converter.obtainTypeFromString(typeName);
+        boolean isVarArg = typeName.endsWith("...");
+        SourcePositionInfo position = null;
+        
+        mCurrentMethod.addParameter(new ParameterInfo(name, typeName, type, isVarArg, position));
+        mCurrentMethod.setVarargs(isVarArg);
+      } else if (qName.equals("exception")) {
+        mCurrentMethod.addException(attributes.getValue("type"));
+      } else if (qName.equals("implements")) {
+        // Resolve interfaces after .xml completely parsed.
+        mApi.mapClassToInterface(mCurrentClass, attributes.getValue("name"));
+      }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) {
+      if (qName.equals("method")) {
+        mCurrentClass.addMethod((MethodInfo) mCurrentMethod);
+      } else if (qName.equals("constructor")) {
+        mCurrentClass.addConstructor((ConstructorInfo) mCurrentMethod);
+      } else if (qName.equals("class") || qName.equals("interface")) {
+        mCurrentPackage.addClass(mCurrentClass);
+        mCurrentClass = mClassScope.pop();
+      } else if (qName.equals("package")) {
+        mApi.addPackage(mCurrentPackage);
+      }
+    }
+
+    public ApiInfo getApi() {
+      return mApi;
+    }
+    
+    private String qualifiedName(String pkg, String className, ClassInfo parent) {
+      String parentQName = (parent != null) ? (parent.qualifiedName() + ".") : "";
+        return pkg + "." + parentQName + className;
+    }
+  }
+  
+  public class Report {
+    private int code;
+    private Set<ErrorMessage> errors;
+    
+    private Report(int code, Set<ErrorMessage> errors) {
+      this.code = code;
+      this.errors = errors;
+    }
+    
+    public int code() {
+      return code;
+    }
+    
+    public Set<ErrorMessage> errors() {
+      return errors;
+    }
+  }
+}
diff --git a/src/com/google/doclava/apicheck/ApiInfo.java b/src/com/google/doclava/apicheck/ApiInfo.java
new file mode 100644
index 0000000..758942a
--- /dev/null
+++ b/src/com/google/doclava/apicheck/ApiInfo.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava.apicheck;
+
+import com.google.doclava.ClassInfo;
+import com.google.doclava.Errors;
+import com.google.doclava.PackageInfo;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ApiInfo {
+
+  private HashMap<String, PackageInfo> mPackages
+      = new HashMap<String, PackageInfo>();
+  private HashMap<String, ClassInfo> mAllClasses
+      = new HashMap<String, ClassInfo>();
+  private Map<ClassInfo,String> mClassToSuper
+      = new HashMap<ClassInfo, String>();
+  private Map<ClassInfo, ArrayList<String>> mClassToInterface
+      = new HashMap<ClassInfo, ArrayList<String>>();
+
+
+  public ClassInfo findClass(String name) {
+    return mAllClasses.get(name);
+  }
+
+  protected void resolveInterfaces() {
+    for (ClassInfo cl : mAllClasses.values()) {
+      ArrayList<String> ifaces = mClassToInterface.get(cl);
+      if (ifaces == null) {
+        continue;
+      }
+      for (String iface : ifaces) {
+        cl.addInterface(mAllClasses.get(iface));
+      }
+    }
+  }
+
+  /**
+   * Checks to see if this api is consistent with a newer version.
+   */
+  public boolean isConsistent(ApiInfo otherApi) {
+    boolean consistent = true;
+    for (PackageInfo pInfo : mPackages.values()) {
+      if (otherApi.getPackages().containsKey(pInfo.name())) {
+        if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()))) {
+          consistent = false;
+        }
+      } else {
+        Errors.error(Errors.REMOVED_PACKAGE, pInfo.position(), "Removed package " + pInfo.name());
+        consistent = false;
+      }
+    }
+    for (PackageInfo pInfo : otherApi.mPackages.values()) {
+      if (!mPackages.containsKey(pInfo.name())) {
+        Errors.error(Errors.ADDED_PACKAGE, pInfo.position(), "Added package " + pInfo.name());
+        consistent = false;
+      }
+    }
+    return consistent;
+  }
+
+  public HashMap<String, PackageInfo> getPackages() {
+    return mPackages;
+  }
+  
+  protected void mapClassToSuper(ClassInfo classInfo, String superclass) {
+    mClassToSuper.put(classInfo, superclass);
+  }
+  
+  protected void mapClassToInterface(ClassInfo classInfo, String iface) {
+    if (!mClassToInterface.containsKey(classInfo)) {
+      mClassToInterface.put(classInfo, new ArrayList<String>());
+    }
+    mClassToInterface.get(classInfo).add(iface);
+  }
+
+  protected void addPackage(PackageInfo pInfo) {
+    // track the set of organized packages in the API
+    mPackages.put(pInfo.name(), pInfo);
+
+    // accumulate a direct map of all the classes in the API
+    for (ClassInfo cl : pInfo.allClasses().values()) {
+      mAllClasses.put(cl.qualifiedName(), cl);
+    }
+  }
+
+  protected void resolveSuperclasses() {
+    for (ClassInfo cl : mAllClasses.values()) {
+      // java.lang.Object has no superclass
+      if (!cl.qualifiedName().equals("java.lang.Object")) {
+        String scName = mClassToSuper.get(cl);
+        if (scName == null) {
+          scName = "java.lang.Object";
+        }
+        ClassInfo superclass = mAllClasses.get(scName);
+        if (superclass == null) {
+          // Superclass not provided by this codebase. Inject a stub.
+          superclass = new ClassInfo(scName);
+        }
+        cl.setSuperClass(superclass);
+      }
+    }
+  }
+}
diff --git a/src/com/google/doclava/apicheck/ApiParseException.java b/src/com/google/doclava/apicheck/ApiParseException.java
new file mode 100644
index 0000000..cd8ad7f
--- /dev/null
+++ b/src/com/google/doclava/apicheck/ApiParseException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 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 com.google.doclava.apicheck;
+
+public final class ApiParseException extends Exception {
+  public ApiParseException(String message, Exception cause) {
+    super(message, cause);
+  }
+  
+  public ApiParseException() {
+    
+  }
+  
+  ApiParseException(String message) {
+    super(message);
+  }
+}