A Class that creates a string hash out of the public API of a
class file.  Useful for caching bytecode.

Review at http://gwt-code-reviews.appspot.com/1359802


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9750 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/javac/BytecodeSignatureMaker.java b/dev/core/src/com/google/gwt/dev/javac/BytecodeSignatureMaker.java
new file mode 100644
index 0000000..8664e0e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/javac/BytecodeSignatureMaker.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2011 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.gwt.dev.javac;
+
+import com.google.gwt.dev.asm.AnnotationVisitor;
+import com.google.gwt.dev.asm.Attribute;
+import com.google.gwt.dev.asm.ClassReader;
+import com.google.gwt.dev.asm.ClassVisitor;
+import com.google.gwt.dev.asm.FieldVisitor;
+import com.google.gwt.dev.asm.MethodVisitor;
+import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.util.Util;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Creates string hashes for various purposes from walking bytecode.
+ */
+public class BytecodeSignatureMaker {
+
+  /**
+   * This visitor looks at public/protected members and methods to compute a
+   * signature. This is intended for determining if a type needs to be
+   * recompiled if byte code it depends on changes.
+   */
+  private static class CompileDependencyVisitor implements ClassVisitor {
+    /**
+     * Mask to strip access bits we don't care about for computing the
+     * signature.
+     */
+    private static final int ACCESS_FILTER_MASK =
+        ~(Opcodes.ACC_DEPRECATED | Opcodes.ACC_NATIVE | Opcodes.ACC_STRICT
+            | Opcodes.ACC_SYNCHRONIZED | Opcodes.ACC_SUPER
+            | Opcodes.ACC_TRANSIENT | Opcodes.ACC_VOLATILE);
+
+    private String header;
+    private Map<String, String> fields = new HashMap<String, String>();
+    private Map<String, String> methods = new HashMap<String, String>();
+
+    public String getSignature() {
+      return Util.computeStrongName(Util.getBytes(getRawString()));
+    }
+
+    public void visit(int version, int access, String name, String signature,
+        String superName, String[] interfaces) {
+      StringBuilder headerBuilder = new StringBuilder();
+      // ignoring version
+      headerBuilder.append(access & ACCESS_FILTER_MASK);
+      headerBuilder.append(":");
+      headerBuilder.append(name);
+      if (signature != null) {
+        headerBuilder.append(":");
+        headerBuilder.append(signature);
+      }
+      if (superName != null) {
+        headerBuilder.append(":");
+        headerBuilder.append(superName);
+      }
+      if (interfaces != null) {
+        Arrays.sort(interfaces);
+        for (String iface : interfaces) {
+          headerBuilder.append(":");
+          headerBuilder.append(iface);
+        }
+      }
+      header = headerBuilder.toString();
+    }
+
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      // ignore
+      return null;
+    }
+
+    public void visitAttribute(Attribute attr) {
+      // ignore
+    }
+
+    public void visitEnd() {
+      // unused
+    }
+
+    public FieldVisitor visitField(int access, String name, String desc,
+        String signature, Object value) {
+      StringBuilder fieldBuilder = new StringBuilder();
+      // We don't care about private or synthetic fields
+      if ((access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC)) == 0) {
+        fieldBuilder.append(access & ACCESS_FILTER_MASK);
+        fieldBuilder.append(":");
+        fieldBuilder.append(name);
+        fieldBuilder.append(":");
+        fieldBuilder.append(desc);
+        if (signature != null) {
+          fieldBuilder.append(":");
+          fieldBuilder.append(signature);
+        }
+        if (value != null) {
+          fieldBuilder.append(":");
+          fieldBuilder.append(value.toString());
+        }
+        fields.put(name, fieldBuilder.toString());
+      }
+
+      // ignoring annotations/attributes on the field.
+      return null;
+    }
+
+    public void visitInnerClass(String name, String outerName,
+        String innerName, int access) {
+      // ignored
+    }
+
+    public MethodVisitor visitMethod(int access, String name, String desc,
+        String signature, String[] exceptions) {
+      // We don't care about private or synthetic methods
+      if ((access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC)) == 0) {
+        StringBuilder methodBuilder = new StringBuilder();
+        methodBuilder.append(access & ACCESS_FILTER_MASK);
+        methodBuilder.append(":");
+        methodBuilder.append(name);
+        methodBuilder.append(":");
+        methodBuilder.append(desc);
+        if (signature != null) {
+          methodBuilder.append(":");
+          methodBuilder.append(signature);
+        }
+        if (exceptions != null) {
+          String[] sortedExceptions = exceptions;
+          Arrays.sort(sortedExceptions);
+          for (String exception : sortedExceptions) {
+            methodBuilder.append(":");
+            methodBuilder.append(exception);
+          }
+        }
+        methods.put(name, methodBuilder.toString());
+      }
+      return null;
+    }
+
+    public void visitOuterClass(String owner, String name, String desc) {
+      // ignored
+    }
+
+    public void visitSource(String source, String debug) {
+      // ignore
+    }
+
+    private String getRawString() {
+      StringBuilder signatureBuilder = new StringBuilder();
+      signatureBuilder.append(header);
+      signatureBuilder.append("|");
+
+      // sort all fields and methods for a deterministic signature.
+      String[] sortedFields = fields.values().toArray(new String[0]);
+      Arrays.sort(sortedFields);
+      for (String field : sortedFields) {
+        signatureBuilder.append(field);
+        signatureBuilder.append("|");
+      }
+
+      String[] sortedMethods = methods.values().toArray(new String[0]);
+      Arrays.sort(sortedMethods);
+      for (String method : sortedMethods) {
+        signatureBuilder.append(method);
+        signatureBuilder.append("|");
+      }
+      return signatureBuilder.toString();
+    }
+  }
+
+  /**
+   * Returns a hash computed from the non-private/non-synthetic members and
+   * methods in a class.
+   * 
+   * @param byteCode byte code for class to analyze.
+   * @return a hex string representing an MD5 digest.
+   */
+  public static String getCompileDependencySignature(byte[] byteCode) {
+    ClassReader reader = new ClassReader(byteCode);
+    CompileDependencyVisitor v = new CompileDependencyVisitor();
+    reader.accept(v, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
+        | ClassReader.SKIP_FRAMES);
+    return v.getSignature();
+  }
+
+  /**
+   * Returns a raw string used to compute the hash from the
+   * non-private/non-synthetic members and methods in a class.
+   * 
+   * @param byteCode byte code for class to analyze.
+   * @return a human readable string of all public API fields
+   */
+  static String getCompileDependencyRawSignature(byte[] byteCode) {
+    ClassReader reader = new ClassReader(byteCode);
+    CompileDependencyVisitor v = new CompileDependencyVisitor();
+    reader.accept(v, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
+        | ClassReader.SKIP_FRAMES);
+    return v.getRawString();
+  }
+
+  private BytecodeSignatureMaker() {
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/javac/BytecodeSignatureMakerTest.java b/dev/core/test/com/google/gwt/dev/javac/BytecodeSignatureMakerTest.java
new file mode 100644
index 0000000..6737d34
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/javac/BytecodeSignatureMakerTest.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright 2011 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.gwt.dev.javac;
+
+import com.google.gwt.dev.javac.impl.MockJavaResource;
+
+import org.eclipse.jdt.core.compiler.CategorizedProblem;
+
+/**
+ * Tests for {@link BytecodeSignatureMaker}
+ */
+public class BytecodeSignatureMakerTest extends CompilationStateTestBase {
+  static final String CLASS_DEP_TYPE_NAME = "test.ClassDependency";
+
+  public void testClassDependencySignature() {
+    final MockJavaResource CLASS_DEP_ORIG =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    // A verbatim copy of CLASS_DEP_ORIG
+    final MockJavaResource CLASS_DEP_NO_CHANGE =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_NO_PRIVATE =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            // Missing fieldPrivate
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            // Missing methodPrivate
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_NO_PROTECTED_FIELD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            // missing fieldProtected
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_NO_DEFAULT_FIELD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            // missing fieldDefault
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_NO_PUBLIC_FIELD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            // missing public field
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_FIELD_VALUE_CHANGE =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            // Value was 100
+            code.append("  static public final int fieldPublicStatic = 99;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_ORDER =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            // re-ordered this field
+            code.append("  public int fieldPublic;\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            // re-ordered this method
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_INNER =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            // Added an inner class definition
+            code.append("  public static class IgnoreMe {\n");
+            code.append("    private int ignoreThisMember;\n");
+            code.append("  }\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_DEPRECATED_FIELD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  @Deprecated\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_DEPRECATED_METHOD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  @Deprecated\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+
+    final MockJavaResource CLASS_DEP_ANNOTATED_FIELD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  @TestAnnotation(\"Foo\")\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_ANNOTATED_METHOD =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  @TestAnnotation(\"Foo\")\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    final MockJavaResource CLASS_DEP_JAVADOC =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  /** a static field */\n");
+            code.append("  static public final int fieldPublicStatic = 100;\n");
+            code.append("  /** a public field */\n");
+            code.append("  public int fieldPublic;\n");
+            code.append("  protected int fieldProtected;\n");
+            code.append("  int fieldDefault;\n");
+            code.append("  private int fieldPrivate;\n");
+            code.append("  /** a public method */\n");
+            code.append("  public int methodPublic() {return 1;};\n");
+            code.append("  protected int methodProtected(String arg) {return 1;};\n");
+            code.append("  int methodDefault() {return 1;};\n");
+            code.append("  private int methodPrivate(){return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+
+    final MockJavaResource TEST_ANNOTATION =
+        new MockJavaResource("test.TestAnnotation") {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public @interface TestAnnotation {\n");
+            code.append("  String value();");
+            code.append("}\n");
+            return code;
+          }
+        };
+    CompiledClass originalClass = buildClass(CLASS_DEP_ORIG);
+    assertNotNull(originalClass);
+
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_NO_CHANGE));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_NO_PRIVATE));
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_NO_PUBLIC_FIELD));
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_NO_PROTECTED_FIELD));
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_NO_DEFAULT_FIELD));
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_FIELD_VALUE_CHANGE));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_ORDER));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_INNER));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_DEPRECATED_FIELD));
+    assertSignaturesEqual(originalClass,
+        buildClass(CLASS_DEP_DEPRECATED_METHOD));
+
+    oracle.add(TEST_ANNOTATION);
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_ANNOTATED_FIELD));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_ANNOTATED_METHOD));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_JAVADOC));
+  }
+
+  public void testClassDependencySignatureWithExceptions() {
+    MockJavaResource ILLEGAL_STATE_EXCEPTION =
+        new MockJavaResource("java.lang.IllegalStateException") {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package java.lang;\n");
+            code.append("public class IllegalStateException extends Throwable {}\n");
+            return code;
+          }
+        };
+    MockJavaResource NUMBER_FORMAT_EXCEPTION =
+        new MockJavaResource("java.lang.NumberFormatException") {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package java.lang;\n");
+            code.append("public class NumberFormatException extends Throwable {}\n");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_EXCEPTION_ORIG =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  public int methodPublic(String arg) ");
+            code.append("      throws IllegalStateException, NumberFormatException {");
+            code.append("    return 1;\n");
+            code.append("  }\n");
+            code.append("}\n");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_EXCEPTION_MOD1 =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            // no exceptions declared
+            code.append("  public int methodPublic(String arg) {return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_EXCEPTION_MOD2 =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            // one exception declared
+            code.append("  public int methodPublic(String arg)");
+            code.append("     throws IllegalStateException {");
+            code.append("    return 1;\n");
+            code.append("  }\n");
+            code.append("}");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_EXCEPTION_MOD3 =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency {\n");
+            code.append("  public int methodPublic(String arg)");
+            // order of declared exceptions is flipped
+            code.append("     throws NumberFormatException, IllegalStateException {");
+            code.append("    return 1;\n");
+            code.append("  }\n");
+            code.append("}");
+            return code;
+          }
+        };
+
+    oracle.add(ILLEGAL_STATE_EXCEPTION);
+    oracle.add(NUMBER_FORMAT_EXCEPTION);
+    CompiledClass originalClass = buildClass(CLASS_DEP_EXCEPTION_ORIG);
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_EXCEPTION_MOD1));
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_EXCEPTION_MOD2));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_EXCEPTION_MOD3));
+  }
+
+  public void testClassDependencySignatureWithGenerics() {
+    MockJavaResource CLASS_DEP_GENERIC_ORIG =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("public class ClassDependency<T> {\n");
+            code.append("  public int methodPublic(T arg) {return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_GENERIC_PARAMETERIZED =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("import java.util.Map;");
+            code.append("public class ClassDependency<T extends Map> {\n");
+            code.append("  public int methodPublic(T arg) {return 1;};\n");
+            code.append("}");
+            return code;
+          }
+        };
+    CompiledClass originalClass = buildClass(CLASS_DEP_GENERIC_ORIG);
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_GENERIC_PARAMETERIZED));
+  }
+
+  public void testClassDependencySignatureWithInterfaces() {
+    MockJavaResource CLASS_DEP_INTERFACE_ORIG =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("import java.util.Map;");
+            code.append("import java.util.Collection;");
+            code.append("public class ClassDependency implements Map, Collection {\n");
+            code.append("  public int methodPublic(String arg) { return 1;}\n");
+            code.append("}\n");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_INTERFACE_MOD1 =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("import java.util.Map;");
+            code.append("import java.util.Collection;");
+            // no interfaces
+            code.append("public class ClassDependency {\n");
+            code.append("  public int methodPublic(String arg) { return 1;}\n");
+            code.append("}\n");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_INTERFACE_MOD2 =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("import java.util.Map;");
+            code.append("import java.util.Collection;");
+            // only one interface
+            code.append("public class ClassDependency implements Map {\n");
+            code.append("  public int methodPublic(String arg) { return 1;}\n");
+            code.append("}\n");
+            return code;
+          }
+        };
+    MockJavaResource CLASS_DEP_INTERFACE_MOD3 =
+        new MockJavaResource(CLASS_DEP_TYPE_NAME) {
+          @Override
+          protected CharSequence getContent() {
+            StringBuffer code = new StringBuffer();
+            code.append("package test;\n");
+            code.append("import java.util.Map;");
+            code.append("import java.util.Collection;");
+            // flipped order of interface decls
+            code.append("public class ClassDependency implements Collection, Map {\n");
+            code.append("  public int methodPublic(String arg) { return 1;}\n");
+            code.append("}\n");
+            return code;
+          }
+        };
+    CompiledClass originalClass = buildClass(CLASS_DEP_INTERFACE_ORIG);
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_INTERFACE_MOD1));
+    assertSignaturesNotEqual(originalClass,
+        buildClass(CLASS_DEP_INTERFACE_MOD2));
+    assertSignaturesEqual(originalClass, buildClass(CLASS_DEP_INTERFACE_MOD3));
+  }
+
+  private void assertSignaturesEqual(CompiledClass original,
+      CompiledClass updated) {
+    String originalSignature =
+        BytecodeSignatureMaker.getCompileDependencySignature(original.getBytes());
+    String updatedSignature =
+        BytecodeSignatureMaker.getCompileDependencySignature(updated.getBytes());
+    if (!originalSignature.equals(updatedSignature)) {
+      String originalRaw =
+          BytecodeSignatureMaker.getCompileDependencyRawSignature(original.getBytes());
+      String updatedRaw =
+          BytecodeSignatureMaker.getCompileDependencyRawSignature(updated.getBytes());
+      fail("Signatures don't match.  raw data expected=<" + originalRaw
+          + "> actual=<" + updatedRaw + ">");
+    }
+  }
+
+  private void assertSignaturesNotEqual(CompiledClass original,
+      CompiledClass updated) {
+    String originalSignature =
+        BytecodeSignatureMaker.getCompileDependencySignature(original.getBytes());
+    String updatedSignature =
+        BytecodeSignatureMaker.getCompileDependencySignature(updated.getBytes());
+    if (originalSignature.equals(updatedSignature)) {
+      String originalRaw =
+          BytecodeSignatureMaker.getCompileDependencyRawSignature(original.getBytes());
+      String updatedRaw =
+          BytecodeSignatureMaker.getCompileDependencyRawSignature(updated.getBytes());
+      fail("Signatures should not match.  raw data expected=<" + originalRaw
+          + "> actual=<" + updatedRaw + ">");
+    }
+  }
+
+  private CompiledClass buildClass(MockJavaResource resource) {
+    oracle.addOrReplace(resource);
+    this.rebuildCompilationState();
+    CompilationUnit unit =
+        state.getCompilationUnitMap().get(resource.getTypeName());
+    assertNotNull(unit);
+    String internalName = resource.getTypeName().replace(".", "/");
+    CategorizedProblem[] problems = unit.getProblems();
+    if (problems != null && problems.length != 0) {
+      fail(problems[0].toString());
+    }
+    for (CompiledClass cc : unit.getCompiledClasses()) {
+      if (cc.getInternalName().equals(internalName)) {
+        return cc;
+      }
+    }
+    fail("Couldn't find class " + internalName + " after compiling.");
+    return null;
+  }
+}