/*
* FindBugs - Find Bugs in Java programs
* Copyright (C) 2005, University of Maryland
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.ba;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import edu.umd.cs.findbugs.util.Util;
import edu.umd.cs.findbugs.xml.XMLOutput;
import edu.umd.cs.findbugs.xml.XMLWriteable;
/**
* Compute a hash of method names and signatures. This allows us to find out
* when a class has been renamed, but not changed in any other obvious way.
*
* @author David Hovemeyer
*/
public class ClassHash implements XMLWriteable, Comparable<ClassHash> {
/**
* XML element name for a ClassHash.
*/
public static final String CLASS_HASH_ELEMENT_NAME = "ClassHash";
/**
* XML element name for a MethodHash.
*/
public static final String METHOD_HASH_ELEMENT_NAME = "MethodHash";
// Fields
private String className;
private byte[] classHash;
private Map<XMethod, MethodHash> methodHashMap;
/**
* Constructor.
*/
public ClassHash() {
this.methodHashMap = new HashMap<XMethod, MethodHash>();
}
/**
* Constructor.
*
* @param classHash
* pre-computed class hash
*/
public ClassHash(String className, byte[] classHash) {
this();
this.className = className;
this.classHash = new byte[classHash.length];
System.arraycopy(classHash, 0, this.classHash, 0, classHash.length);
}
/**
* Set method hash for given method.
*
* @param method
* the method
* @param methodHash
* the method hash
*/
public void setMethodHash(XMethod method, byte[] methodHash) {
methodHashMap.put(method, new MethodHash(method.getName(), method.getSignature(), method.isStatic(), methodHash));
}
/**
* @return Returns the className.
*/
public String getClassName() {
return className;
}
/**
* Get class hash.
*
* @return the class hash
*/
public byte[] getClassHash() {
return classHash;
}
/**
* Set class hash.
*
* @param classHash
* the class hash value to set
*/
public void setClassHash(byte[] classHash) {
this.classHash = new byte[classHash.length];
System.arraycopy(classHash, 0, this.classHash, 0, classHash.length);
}
/**
* Get method hash for given method.
*
* @param method
* the method
* @return the MethodHash
*/
public MethodHash getMethodHash(XMethod method) {
return methodHashMap.get(method);
}
/**
* Compute hash for given class and all of its methods.
*
* @param javaClass
* the class
* @return this object
*/
public ClassHash computeHash(JavaClass javaClass) {
this.className = javaClass.getClassName();
Method[] methodList = new Method[javaClass.getMethods().length];
// Sort methods
System.arraycopy(javaClass.getMethods(), 0, methodList, 0, javaClass.getMethods().length);
Arrays.sort(methodList, new Comparator<Method>() {
public int compare(Method o1, Method o2) {
// sort by name, then signature
int cmp = o1.getName().compareTo(o2.getName());
if (cmp != 0)
return cmp;
return o1.getSignature().compareTo(o2.getSignature());
}
});
Field[] fieldList = new Field[javaClass.getFields().length];
// Sort fields
System.arraycopy(javaClass.getFields(), 0, fieldList, 0, javaClass.getFields().length);
Arrays.sort(fieldList, new Comparator<Field>() {
/*
* (non-Javadoc)
*
* @see java.util.Comparator#compare(T, T)
*/
public int compare(Field o1, Field o2) {
int cmp = o1.getName().compareTo(o2.getName());
if (cmp != 0)
return cmp;
return o1.getSignature().compareTo(o2.getSignature());
}
});
MessageDigest digest = Util.getMD5Digest();
// Compute digest of method names and signatures, in order.
// Also, compute method hashes.
CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
for (Method method : methodList) {
work(digest, method.getName(), encoder);
work(digest, method.getSignature(), encoder);
MethodHash methodHash = new MethodHash().computeHash(method);
methodHashMap.put(XFactory.createXMethod(javaClass, method), methodHash);
}
// Compute digest of field names and signatures.
for (Field field : fieldList) {
work(digest, field.getName(), encoder);
work(digest, field.getSignature(), encoder);
}
classHash = digest.digest();
return this;
}
private static void work(MessageDigest digest, String s, CharsetEncoder encoder) {
try {
CharBuffer cbuf = CharBuffer.allocate(s.length());
cbuf.put(s);
cbuf.flip();
ByteBuffer buf = encoder.encode(cbuf);
// System.out.println("pos="+buf.position() +",limit=" +
// buf.limit());
int nbytes = buf.limit();
byte[] encodedBytes = new byte[nbytes];
buf.get(encodedBytes);
digest.update(encodedBytes);
} catch (CharacterCodingException e) {
// This should never happen, since we're encoding to UTF-8.
}
}
public void writeXML(XMLOutput xmlOutput) throws IOException {
xmlOutput.startTag(CLASS_HASH_ELEMENT_NAME);
xmlOutput.addAttribute("class", className);
xmlOutput.addAttribute("value", hashToString(classHash));
xmlOutput.stopTag(false);
for (Map.Entry<XMethod, MethodHash> entry : methodHashMap.entrySet()) {
xmlOutput.startTag(METHOD_HASH_ELEMENT_NAME);
xmlOutput.addAttribute("name", entry.getKey().getName());
xmlOutput.addAttribute("signature", entry.getKey().getSignature());
xmlOutput.addAttribute("isStatic", String.valueOf(entry.getKey().isStatic()));
xmlOutput.addAttribute("value", hashToString(entry.getValue().getMethodHash()));
xmlOutput.stopTag(true);
}
xmlOutput.closeTag(CLASS_HASH_ELEMENT_NAME);
}
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', };
/**
* Convert a hash to a string of hex digits.
*
* @param hash
* the hash
* @return a String representation of the hash
*/
public static String hashToString(byte[] hash) {
StringBuilder buf = new StringBuilder();
for (byte b : hash) {
buf.append(HEX_CHARS[(b >> 4) & 0xF]);
buf.append(HEX_CHARS[b & 0xF]);
}
return buf.toString();
}
private static int hexDigitValue(char c) {
if (c >= '0' && c <= '9')
return c - '0';
else if (c >= 'a' && c <= 'f')
return 10 + (c - 'a');
else if (c >= 'A' && c <= 'F')
return 10 + (c - 'A');
else
throw new IllegalArgumentException("Illegal hex character: " + c);
}
/**
* Convert a string of hex digits to a hash.
*
* @param s
* string of hex digits
* @return the hash value represented by the string
*/
public static byte[] stringToHash(String s) {
if (s.length() % 2 != 0)
throw new IllegalArgumentException("Invalid hash string: " + s);
byte[] hash = new byte[s.length() / 2];
for (int i = 0; i < s.length(); i += 2) {
byte b = (byte) ((hexDigitValue(s.charAt(i)) << 4) + hexDigitValue(s.charAt(i + 1)));
hash[i / 2] = b;
}
return hash;
}
/**
* Return whether or not this class hash has the same hash value as the one
* given.
*
* @param other
* another ClassHash
* @return true if the hash values are the same, false if not
*/
public boolean isSameHash(ClassHash other) {
return Arrays.equals(classHash, other.classHash);
}
@Override
public int hashCode() {
if (classHash == null)
return 0;
int result = 1;
for (byte element : classHash)
result = 31 * result + element;
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ClassHash))
return false;
return isSameHash((ClassHash) o);
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(T)
*/
public int compareTo(ClassHash other) {
int cmp = MethodHash.compareHashes(this.classHash, other.classHash);
// System.out.println(this + " <=> " + other + ": compareTo=" + cmp);
return cmp;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getClassName() + ":" + hashToString(this.classHash);
}
}