package org.objectweb.asm; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.TypeAnnotationVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.commons.EmptyVisitor; /** * An AnnotationVerifier provides a way to check to see if two * versions of the same class (from two different .class files), * have the same annotations on the same elements. */ public class AnnotationVerifier { /** * The "correct" version of the class to verify against. */ private ClassRecorder originalVisitor; /** * The uncertain version of the class to verify. */ private ClassRecorder newVisitor; /** * Constructs a new AnnotationVerifier that does not yet have * any information about the class. */ public AnnotationVerifier() { originalVisitor = new ClassRecorder(); newVisitor = new ClassRecorder(); } /** * Returns the ClassVisitor which should be made to visit the * version of the class known to be correct. * * @return a visitor for the good version of the class */ public ClassVisitor originalVisitor() { return originalVisitor; } /** * Returns the ClassVisitor which should be made to visit the * version of the class being tested. * * @return a visitor the the experimental version of the class */ public ClassVisitor newVisitor() { return newVisitor; } /** * Verifies that the visitors returned by {@link #originalVisitor()} and * {@link #newVisitor()} have visited the same class. This method can only * be called if both visitors have already visited a class. * * @throws AnnotationMismatchException if the two visitors have not visited * two versions of the same class that contain idential annotations. */ public void verify() { if(!newVisitor.name.equals(originalVisitor.name)) { throw new AnnotationMismatchException( "Cannot verify two different classes " + newVisitor.name + " cannot be verified against " + originalVisitor.name); } newVisitor.verifyAgainst(originalVisitor); } /** * A ClassRecorder records all the annotations that it visits, and serves * as a ClassVisitor, FieldVisitor and MethodVisitor. */ private class ClassRecorder extends EmptyVisitor { private String description; public String name; private String signature; private Map fieldRecorders; // key is unparameterized name private Map methodRecorders; // key is complete method signature // general annotations private Map anns; private Map xanns; //method specific annotations private Set danns; private Map panns; public ClassRecorder() { this("[class: ","",""); } public ClassRecorder(String internalDescription, String name, String signature) { this.description = internalDescription; this.name = name; this.signature = signature; fieldRecorders = new HashMap(); methodRecorders = new HashMap(); anns = new HashMap(); xanns = new HashMap(); danns = new HashSet(); panns = new HashMap(); } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.name = name; this.signature = signature; description = description + name; } public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationRecorder av = new AnnotationRecorder( description + " annotation: " + desc); anns.put(desc,av); return av; } public TypeAnnotationVisitor visitTypeAnnotation(String desc, boolean visible) { AnnotationRecorder av = new AnnotationRecorder( description + " annotation: " + desc); xanns.put(desc,av); return av; } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { ClassRecorder fr = new ClassRecorder( description + " field: " + name, name, signature); fieldRecorders.put(name, fr); return fr; } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { ClassRecorder mr = new ClassRecorder( description + " method: " + name + desc, name+desc, signature); methodRecorders.put(name+desc, mr); return mr; } // MethodVisitor methods: public AnnotationVisitor visitAnnotationDefault() { AnnotationRecorder dr = new AnnotationRecorder( description + " default annotation"); danns.add(dr); return dr; } public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { ParameterDescription pd = new ParameterDescription(parameter, desc, visible); AnnotationRecorder pr = new AnnotationRecorder(description + " parameter annotation: " + pd); panns.put(pd, pr); return pr; } public void verifyAgainst(ClassRecorder correct) { // first, ensure all annotations are correct verifyAnns(this.anns, correct.anns); verifyAnns(this.xanns, correct.xanns); // then recurse into any annotations on fields/methods verifyMemberAnns(this.fieldRecorders, correct.fieldRecorders); verifyMemberAnns(this.methodRecorders, correct.methodRecorders); } private void verifyAnns( Map questionableAnns, Map correctAnns) { Set unresolvedQuestionableAnns = new HashSet(questionableAnns.values()); for(Map.Entry entry : correctAnns.entrySet()) { String name = entry.getKey(); AnnotationRecorder correctAnn = entry.getValue(); AnnotationRecorder questionableAnn = questionableAnns.get(name); if(questionableAnn == null) { throw new AnnotationMismatchException(description + " does not contain expected annotation: " + correctAnn); } questionableAnn.verifyAgainst(correctAnn); unresolvedQuestionableAnns.remove(questionableAnn); } for(AnnotationRecorder unexpectedAnnOnThis : unresolvedQuestionableAnns) { throw new AnnotationMismatchException(description + " contains unexpected annotation : " + unexpectedAnnOnThis); } } private void verifyMemberAnns( Map questionableMembers, Map correctMembers) { Set unresolvedQuestionableMembers = new HashSet(questionableMembers.values()); for(Map.Entry entry : correctMembers.entrySet()) { String name = entry.getKey(); ClassRecorder correctMember = entry.getValue(); ClassRecorder questionableMember = questionableMembers.get(name); if(questionableMember == null) { throw new AnnotationMismatchException(description + " does not contain expected member: " + correctMember); } questionableMember.verifyAgainst(correctMember); unresolvedQuestionableMembers.remove(questionableMember); } for(ClassRecorder unexpectedMemberOnThis : unresolvedQuestionableMembers) { System.out.println("Going to throw exception: "); System.out.println("questionable: " + questionableMembers); System.out.println("correct: " + correctMembers); throw new AnnotationMismatchException(description + " contains unexpected member: " + unexpectedMemberOnThis); } } public String toString() { return description; } } /** * An AnnotationRecorder is an TypeAnnotationVisitor that records all the * information it visits. */ private class AnnotationRecorder implements TypeAnnotationVisitor { private String description; private List fieldArgs1; private List fieldArgs2; private List enumArgs1; private List enumArgs2; private List enumArgs3; private List innerAnnotationArgs1; private List innerAnnotationArgs2; private Map innerAnnotationMap; private List arrayArgs; private Map arrayMap; private List xIndexArgs; private List xLengthArgs; private List xLocationArgs; private List xLocationLengthArgs; private List xOffsetArgs; private List xStartPcArgs; private List xTargetTypeArgs; private List xParamIndexArgs; private List xBoundIndexArgs; private List xTypeIndexArgs; public AnnotationRecorder(String description) { this.description = description; fieldArgs1 = new ArrayList(); fieldArgs2 = new ArrayList(); enumArgs1 = new ArrayList(); enumArgs2 = new ArrayList(); enumArgs3 = new ArrayList(); innerAnnotationArgs1 = new ArrayList(); innerAnnotationArgs2 = new ArrayList(); innerAnnotationMap = new HashMap(); arrayArgs = new ArrayList(); arrayMap = new HashMap(); xIndexArgs = new ArrayList(); xLengthArgs = new ArrayList(); xLocationArgs = new ArrayList(); xLocationLengthArgs = new ArrayList(); xOffsetArgs = new ArrayList(); xStartPcArgs = new ArrayList(); xTargetTypeArgs = new ArrayList(); xParamIndexArgs = new ArrayList(); xBoundIndexArgs = new ArrayList(); xTypeIndexArgs = new ArrayList(); } public void visitXIndex(int index) { xIndexArgs.add(index); } public void visitXLength(int length) { xLengthArgs.add(length); } public void visitXLocation(int location) { xLocationArgs.add(location); } public void visitXLocationLength(int location_length) { xLocationLengthArgs.add(location_length); } public void visitXOffset(int offset) { xOffsetArgs.add(offset); } @Override public void visitXNumEntries(int num_entries) { } public void visitXStartPc(int start_pc) { xStartPcArgs.add(start_pc); } public void visitXTargetType(int target_type) { xTargetTypeArgs.add(target_type); } public void visitXParamIndex(int param_index) { xParamIndexArgs.add(param_index); } public void visitXBoundIndex(int bound_index) { xBoundIndexArgs.add(bound_index); } public void visitXTypeIndex(int type_index) { xTypeIndexArgs.add(type_index); } public void visit(String name, Object value) { fieldArgs1.add(name); fieldArgs2.add(value); } public AnnotationVisitor visitAnnotation(String name, String desc) { innerAnnotationArgs1.add(name); innerAnnotationArgs2.add(name); AnnotationRecorder av= new AnnotationRecorder(description + name); innerAnnotationMap.put(name, av); return av; } public AnnotationVisitor visitArray(String name) { arrayArgs.add(name); AnnotationRecorder av = new AnnotationRecorder(description + name); arrayMap.put(name, av); return av; } public void visitEnd() { } public void visitEnum(String name, String desc, String value) { enumArgs1.add(name); enumArgs2.add(desc); enumArgs3.add(value); } public String toString() { return description; } /** * Checks that the information passed into this matches the information * passed into another AnnotationRecorder. For right now, the order in * which information is passed in does matter. If there is a conflict in * information, an exception will be thrown. * * @param ar an annotation recorder that has visited the correct information * this should visit * @throws AnnotationMismatchException if the information visited by this * does not match the information in ar */ public void verifyAgainst(AnnotationRecorder ar) { StringBuilder sb = new StringBuilder(); verifyList(sb, "visit()", 1, this.fieldArgs1, ar.fieldArgs1); verifyList(sb, "visit()", 2, this.fieldArgs2, ar.fieldArgs2); verifyList(sb, "visitEnum()", 1, this.enumArgs1, ar.enumArgs1); verifyList(sb, "visitEnum()", 2, this.enumArgs2, ar.enumArgs2); verifyList(sb, "visitEnum()", 3, this.enumArgs3, ar.enumArgs3); verifyList(sb, "visitAnnotation()", 1, this.innerAnnotationArgs1, ar.innerAnnotationArgs1); verifyList(sb, "visitAnnotation()", 2, this.innerAnnotationArgs2, ar.innerAnnotationArgs2); verifyList(sb, "visitArray()", 1, this.arrayArgs, ar.arrayArgs); verifyList(sb, "visitXIndexArgs()", 1, this.xIndexArgs, ar.xIndexArgs); verifyList(sb, "visitXLength()", 1, this.xLengthArgs, ar.xIndexArgs); verifyList(sb, "visitXLocation()", 1, this.xLocationArgs, ar.xLocationArgs); verifyList(sb, "visitXLocationLength()", 1, this.xLocationLengthArgs, ar.xLocationLengthArgs); verifyList(sb, "visitXOffset()", 1, this.xOffsetArgs, ar.xOffsetArgs); verifyList(sb, "visitXStartPc()", 1, this.xStartPcArgs, ar.xStartPcArgs); verifyList(sb, "visitXTargetType()", 1, this.xTargetTypeArgs, ar.xTargetTypeArgs); verifyList(sb, "visitXParamIndex()", 1, this.xParamIndexArgs, ar.xParamIndexArgs); verifyList(sb, "visitXBoundIndex()", 1, this.xBoundIndexArgs, ar.xBoundIndexArgs); verifyList(sb, "visitXTypeIndex()", 1, this.xTypeIndexArgs, ar.xTypeIndexArgs); verifyInnerAnnotationRecorder(sb, this.innerAnnotationMap, ar.innerAnnotationMap); verifyInnerAnnotationRecorder(sb, this.arrayMap, ar.arrayMap); if(sb.length() > 0) { throw new AnnotationMismatchException(sb.toString()); } } private void verifyList( StringBuilder sb, String methodName, int parameter, List questionable, List correct) { if(!questionable.equals(correct)) { String s = "\n" + description + " was called with unexpected information in parameter: " + parameter + "\nReceived: " + questionable + "\nExpected: " + correct; } } private void verifyInnerAnnotationRecorder( StringBuilder sb, Map questionableAR, Map correctAR) { // checks on arguments passed in to the methods that created these // AnnotationRecorders (i.e. the checks on the String keys on these maps) // ensure that these have identical keys for(Map.Entry questionableEntry : questionableAR.entrySet()) { questionableEntry.getValue().verifyAgainst( correctAR.get(questionableEntry.getClass())); } } } /** * A ParameterDescription is a convenient class used to keep information about * method parameters. Parameters are equal if they have the same index, * regardless of their description. */ private class ParameterDescription { public final int parameter; public final String desc; public final boolean visible; public ParameterDescription(int parameter, String desc, boolean visible) { this.parameter = parameter; this.desc = desc; this.visible = visible; } public boolean equals(Object o) { if(o instanceof ParameterDescription) { ParameterDescription p = (ParameterDescription) o; return this.parameter == p.parameter; } return false; } public int hashCode() { return parameter * 17; } public String toString() { return "parameter index: " + parameter + " desc: " + desc + " visible: " + visible; } } /** * An AnnotationMismatchException is an Exception that indicates that * two versions of the same class do not have the same annotations on * either the class, its field, or its methods. */ public class AnnotationMismatchException extends RuntimeException { private static final long serialVersionUID = 20060714L; // today's date /** * Constructs a new AnnotationMismatchException with the given error message. * * @param msg the error as to why the annotations do not match. */ public AnnotationMismatchException(String msg) { super(msg); } } }