/*
* 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.model;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.JavaClassAndMethod;
import edu.umd.cs.findbugs.ba.SignatureParser;
import edu.umd.cs.findbugs.xml.XMLAttributeList;
import edu.umd.cs.findbugs.xml.XMLOutput;
import edu.umd.cs.findbugs.xml.XMLWriteable;
/**
* Features of a class which may be used to identify it if it is renamed or
* modified.
*
* @author David Hovemeyer
*/
public class ClassFeatureSet implements XMLWriteable {
public static final String CLASS_NAME_KEY = "Class:";
public static final String METHOD_NAME_KEY = "Method:";
public static final String CODE_LENGTH_KEY = "CodeLength:";
public static final String FIELD_NAME_KEY = "Field:";
private String className;
private boolean isInterface;
private Set<String> featureSet;
/**
* Constructor. Creates an empty feature set.
*/
public ClassFeatureSet() {
this.featureSet = new HashSet<String>();
}
/**
* Minimum code length required to add a CodeLength feature.
*/
public static final int MIN_CODE_LENGTH = 10;
/**
* Initialize from given JavaClass.
*
* @param javaClass
* the JavaClass
* @return this object
*/
public ClassFeatureSet initialize(JavaClass javaClass) {
this.className = javaClass.getClassName();
this.isInterface = javaClass.isInterface();
addFeature(CLASS_NAME_KEY + transformClassName(javaClass.getClassName()));
for (Method method : javaClass.getMethods()) {
if (!isSynthetic(method)) {
String transformedMethodSignature = transformMethodSignature(method.getSignature());
if (method.isStatic() || !overridesSuperclassMethod(javaClass, method)) {
addFeature(METHOD_NAME_KEY + method.getName() + ":" + transformedMethodSignature);
}
Code code = method.getCode();
if (code != null && code.getCode() != null && code.getCode().length >= MIN_CODE_LENGTH) {
addFeature(CODE_LENGTH_KEY + method.getName() + ":" + transformedMethodSignature + ":"
+ code.getCode().length);
}
}
}
for (Field field : javaClass.getFields()) {
if (!isSynthetic(field)) {
addFeature(FIELD_NAME_KEY + field.getName() + ":" + transformSignature(field.getSignature()));
}
}
return this;
}
/**
* Determine if given method overrides a superclass or superinterface
* method.
*
* @param javaClass
* class defining the method
* @param method
* the method
* @return true if the method overrides a superclass/superinterface method,
* false if not
* @throws ClassNotFoundException
*/
private boolean overridesSuperclassMethod(JavaClass javaClass, Method method) {
if (method.isStatic())
return false;
try {
JavaClass[] superclassList = javaClass.getSuperClasses();
if (superclassList != null) {
JavaClassAndMethod match = Hierarchy.findMethod(superclassList, method.getName(), method.getSignature(),
Hierarchy.INSTANCE_METHOD);
if (match != null)
return true;
}
JavaClass[] interfaceList = javaClass.getAllInterfaces();
if (interfaceList != null) {
JavaClassAndMethod match = Hierarchy.findMethod(interfaceList, method.getName(), method.getSignature(),
Hierarchy.INSTANCE_METHOD);
if (match != null)
return true;
}
return false;
} catch (ClassNotFoundException e) {
return true;
}
}
/**
* Figure out if a class member (field or method) is synthetic.
*
* @param member
* a field or method
* @return true if the member is synthetic
*/
private boolean isSynthetic(FieldOrMethod member) {
if (member.isSynthetic()) // this never works, but worth a try
return true;
String name = member.getName();
if (name.startsWith("class$"))
return true;
if (name.startsWith("access$"))
return true;
return false;
}
/**
* @return Returns the className.
*/
public String getClassName() {
return className;
}
/**
* @param className
* The className to set.
*/
public void setClassName(String className) {
this.className = className;
}
/**
* @return Returns the isInterface.
*/
public boolean isInterface() {
return isInterface;
}
/**
* @param isInterface
* The isInterface to set.
*/
public void setInterface(boolean isInterface) {
this.isInterface = isInterface;
}
public int getNumFeatures() {
return featureSet.size();
}
public void addFeature(String feature) {
featureSet.add(feature);
}
public Iterator<String> featureIterator() {
return featureSet.iterator();
}
public boolean hasFeature(String feature) {
return featureSet.contains(feature);
}
/**
* Transform a class name by stripping its package name.
*
* @param className
* a class name
* @return the transformed class name
*/
public static String transformClassName(String className) {
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0) {
String pkg = className.substring(0, lastDot);
if (!isUnlikelyToBeRenamed(pkg)) {
className = className.substring(lastDot + 1);
}
}
return className;
}
/**
* Return true if classes in the given package is unlikely to be renamed:
* e.g., because they are part of a public API.
*
* @param pkg
* the package name
* @return true if classes in the package is unlikely to be renamed
*/
public static boolean isUnlikelyToBeRenamed(String pkg) {
return pkg.startsWith("java.");
}
/**
* Transform a method signature to allow it to be compared even if any of
* its parameter types are moved to another package.
*
* @param signature
* a method signature
* @return the transformed signature
*/
public static String transformMethodSignature(String signature) {
StringBuilder buf = new StringBuilder();
buf.append('(');
SignatureParser parser = new SignatureParser(signature);
for (Iterator<String> i = parser.parameterSignatureIterator(); i.hasNext();) {
String param = i.next();
param = transformSignature(param);
buf.append(param);
}
buf.append(')');
return buf.toString();
}
/**
* Transform a field or method parameter signature to allow it to be
* compared even if it is moved to another package.
*
* @param signature
* the signature
* @return the transformed signature
*/
public static String transformSignature(String signature) {
StringBuilder buf = new StringBuilder();
int lastBracket = signature.lastIndexOf('[');
if (lastBracket > 0) {
buf.append(signature.substring(0, lastBracket + 1));
signature = signature.substring(lastBracket + 1);
}
if (signature.startsWith("L")) {
signature = signature.substring(1, signature.length() - 1).replace('/', '.');
signature = transformClassName(signature);
signature = "L" + signature.replace('.', '/') + ";";
}
buf.append(signature);
return buf.toString();
}
/**
* Minimum number of features which must be present in order to declare two
* classes similar.
*/
public static final int MIN_FEATURES = 5;
/**
* Minimum similarity required to declare two classes similar.
*/
public static final double MIN_MATCH = 0.60;
/**
* Similarity of classes which don't have enough features to match exactly,
* but whose class names match exactly.
*/
public static final double EXACT_CLASS_NAME_MATCH = MIN_MATCH + 0.1;
public static double similarity(ClassFeatureSet a, ClassFeatureSet b) {
// Some features must match exactly
if (a.isInterface() != b.isInterface())
return 0.0;
if (a.getNumFeatures() < MIN_FEATURES || b.getNumFeatures() < MIN_FEATURES)
return a.getClassName().equals(b.getClassName()) ? EXACT_CLASS_NAME_MATCH : 0.0;
int numMatch = 0;
int max = Math.max(a.getNumFeatures(), b.getNumFeatures());
for (Iterator<String> i = a.featureIterator(); i.hasNext();) {
String feature = i.next();
if (b.hasFeature(feature)) {
++numMatch;
}
}
return ((double) numMatch / (double) max);
}
public boolean similarTo(ClassFeatureSet other) {
return similarity(this, other) >= MIN_MATCH;
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: " + ClassFeatureSet.class.getName() + " <class 1> <class 2>");
System.exit(1);
}
JavaClass a = Repository.lookupClass(args[0]);
JavaClass b = Repository.lookupClass(args[1]);
ClassFeatureSet aFeatures = new ClassFeatureSet().initialize(a);
ClassFeatureSet bFeatures = new ClassFeatureSet().initialize(b);
System.out.println("Similarity is " + similarity(aFeatures, bFeatures));
System.out.println("Classes are" + (aFeatures.similarTo(bFeatures) ? "" : " not") + " similar");
}
public static final String ELEMENT_NAME = "ClassFeatureSet";
public static final String FEATURE_ELEMENT_NAME = "Feature";
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.xml.XMLWriteable#writeXML(edu.umd.cs.findbugs.xml
* .XMLOutput)
*/
public void writeXML(XMLOutput xmlOutput) throws IOException {
xmlOutput.openTag(ELEMENT_NAME, new XMLAttributeList().addAttribute("class", className));
for (Iterator<String> i = featureIterator(); i.hasNext();) {
String feature = i.next();
xmlOutput.openCloseTag(FEATURE_ELEMENT_NAME, new XMLAttributeList().addAttribute("value", feature));
}
xmlOutput.closeTag(ELEMENT_NAME);
}
}