/*
* 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.workflow;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import edu.umd.cs.findbugs.AppVersion;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.FindBugs;
import edu.umd.cs.findbugs.SortedBugCollection;
import edu.umd.cs.findbugs.charsets.UTF8;
import edu.umd.cs.findbugs.config.CommandLine;
/**
* Mine historical information from a BugCollection. The BugCollection should be
* built using UpdateBugCollection to record the history of analyzing all
* versions over time.
*
* @author David Hovemeyer
* @author William Pugh
*/
public class MineBugHistory {
/**
*
*/
private static final int WIDTH = 12;
static final int ADDED = 0;
static final int NEWCODE = 1;
static final int REMOVED = 2;
static final int REMOVEDCODE = 3;
static final int RETAINED = 4;
static final int DEAD = 5;
static final int ACTIVE_NOW = 6;
static final int TUPLE_SIZE = 7;
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH);
static class Version {
long sequence;
int tuple[] = new int[TUPLE_SIZE];
Version(long sequence) {
this.sequence = sequence;
}
/**
* @return Returns the sequence.
*/
public long getSequence() {
return sequence;
}
void increment(int key) {
tuple[key]++;
if (key == ADDED || key == RETAINED || key == NEWCODE)
tuple[ACTIVE_NOW]++;
}
int get(int key) {
return tuple[key];
}
}
SortedBugCollection bugCollection;
Version[] versionList;
Map<Long, AppVersion> sequenceToAppVersionMap = new HashMap<Long, AppVersion>();
boolean formatDates = false;
boolean noTabs = false;
boolean summary = false;
boolean xml = false;
public MineBugHistory() {
}
public MineBugHistory(SortedBugCollection bugCollection) {
this.bugCollection = bugCollection;
}
public void setBugCollection(SortedBugCollection bugCollection) {
this.bugCollection = bugCollection;
}
public void setFormatDates(boolean value) {
this.formatDates = value;
}
public void setNoTabs() {
this.xml = false;
this.noTabs = true;
this.summary = false;
}
public void setXml() {
this.xml = true;
this.noTabs = false;
this.summary = false;
}
public void setSummary() {
this.xml = false;
this.summary = true;
this.noTabs = false;
}
public MineBugHistory execute() {
long sequenceNumber = bugCollection.getSequenceNumber();
int maxSequence = (int) sequenceNumber;
versionList = new Version[maxSequence + 1];
for (int i = 0; i <= maxSequence; ++i) {
versionList[i] = new Version(i);
}
for (Iterator<AppVersion> i = bugCollection.appVersionIterator(); i.hasNext();) {
AppVersion appVersion = i.next();
long versionSequenceNumber = appVersion.getSequenceNumber();
sequenceToAppVersionMap.put(versionSequenceNumber, appVersion);
}
AppVersion currentAppVersion = bugCollection.getCurrentAppVersion();
sequenceToAppVersionMap.put(sequenceNumber, currentAppVersion);
for (Iterator<BugInstance> j = bugCollection.iterator(); j.hasNext();) {
BugInstance bugInstance = j.next();
for (int i = 0; i <= maxSequence; ++i) {
if (bugInstance.getFirstVersion() > i)
continue;
boolean activePrevious = bugInstance.getFirstVersion() < i
&& (!bugInstance.isDead() || bugInstance.getLastVersion() >= i - 1);
boolean activeCurrent = !bugInstance.isDead() || bugInstance.getLastVersion() >= i;
int key = getKey(activePrevious, activeCurrent);
if (key == REMOVED && !bugInstance.isRemovedByChangeOfPersistingClass())
key = REMOVEDCODE;
else if (key == ADDED && !bugInstance.isIntroducedByChangeOfExistingClass())
key = NEWCODE;
versionList[i].increment(key);
}
}
return this;
}
public void dump(PrintStream out) {
if (xml)
dumpXml(out);
else if (noTabs)
dumpNoTabs(out);
else if (summary)
dumpSummary(out);
else
dumpOriginal(out);
}
public void dumpSummary(PrintStream out) {
StringBuilder b = new StringBuilder();
for (int i = Math.max(0, versionList.length - 10); i < versionList.length; ++i) {
Version version = versionList[i];
int added = version.get(ADDED) + version.get(NEWCODE);
int removed = version.get(REMOVED) + version.get(REMOVEDCODE);
b.append(" ");
if (added > 0) {
b.append('+');
b.append(added);
}
if (removed > 0) {
b.append('-');
b.append(removed);
}
if (added == 0 && removed == 0)
b.append('0');
int paddingNeeded = WIDTH - b.length() % WIDTH;
if (paddingNeeded > 0)
b.append(" ".substring(0, paddingNeeded));
}
int errors = bugCollection.getErrors().size();
if (errors > 0)
b.append(" ").append(errors).append(" errors");
out.println(b.toString());
}
/** This is how dump() was implemented up to and including version 0.9.5. */
public void dumpOriginal(PrintStream out) {
out.println("seq\tversion\ttime\tclasses\tNCSS\tadded\tnewCode\tfixed\tremoved\tretained\tdead\tactive");
for (int i = 0; i < versionList.length; ++i) {
Version version = versionList[i];
AppVersion appVersion = sequenceToAppVersionMap.get(version.getSequence());
out.print(i);
out.print('\t');
out.print(appVersion != null ? appVersion.getReleaseName() : "");
out.print('\t');
if (formatDates)
out.print("\"" + (appVersion != null ? dateFormat.format(new Date(appVersion.getTimestamp())) : "") + "\"");
else
out.print(appVersion != null ? appVersion.getTimestamp() / 1000 : 0L);
out.print('\t');
if (appVersion != null) {
out.print(appVersion.getNumClasses());
out.print('\t');
out.print(appVersion.getCodeSize());
} else
out.print("\t0\t0");
for (int j = 0; j < TUPLE_SIZE; ++j) {
out.print('\t');
out.print(version.get(j));
}
out.println();
}
}
/** emit <code>width</code> space characters to <code>out</code> */
private static void pad(int width, PrintStream out) {
while (width-- > 0)
out.print(' ');
}
/**
* equivalent to out.print(obj) except it may be padded on the left or right
*
* @param width
* padding will occur if the stringified oxj is shorter than this
* @param alignRight
* true to pad on the left, false to pad on the right
* @param out
* the PrintStream printed to
* @param obj
* the value to print (may be an auto-boxed primitive)
*/
private static void print(int width, boolean alignRight, PrintStream out, Object obj) {
String s = String.valueOf(obj);
int padLen = width - s.length();
if (alignRight)
pad(padLen, out);
out.print(s); // doesn't truncate if (s.length() > width)
if (!alignRight)
pad(padLen, out);
}
/**
* This implementation of dump() tries to better align columns (when viewed
* with a fixed-width font) by padding with spaces instead of using tabs.
* Also, timestamps are formatted more tersely (-formatDates option). The
* bad news is that it requires a minimum of 112 columns.
*
* @see #dumpOriginal(PrintStream)
*/
public void dumpNoTabs(PrintStream out) {
// out.println("seq\tversion\ttime\tclasses\tNCSS\tadded\tnewCode\tfixed\tremoved\tretained\tdead\tactive");
print(3, true, out, "seq");
out.print(' ');
print(19, false, out, "version");
out.print(' ');
print(formatDates ? 12 : 10, false, out, "time");
print(1 + 7, true, out, "classes");
print(1 + WIDTH, true, out, "NCSS");
print(1 + WIDTH, true, out, "added");
print(1 + WIDTH, true, out, "newCode");
print(1 + WIDTH, true, out, "fixed");
print(1 + WIDTH, true, out, "removed");
print(1 + WIDTH, true, out, "retained");
print(1 + WIDTH, true, out, "dead");
print(1 + WIDTH, true, out, "active");
out.println();
// note: if we were allowed to depend on JDK 1.5 we could use
// out.printf():
// Object line[] = { "seq", "version", "time", "classes", "NCSS",
// "added", "newCode", "fixed", "removed", "retained", "dead", "active"
// };
// out.printf("%3s %-19s %-16s %7s %7s %7s %7s %7s %7s %8s %6s %7s%n",
// line);
for (int i = 0; i < versionList.length; ++i) {
Version version = versionList[i];
AppVersion appVersion = sequenceToAppVersionMap.get(version.getSequence());
print(3, true, out, i); // out.print(i);
out.print(' '); // '\t'
print(19, false, out, appVersion != null ? appVersion.getReleaseName() : "");
out.print(' ');
long ts = (appVersion != null ? appVersion.getTimestamp() : 0L);
if (formatDates)
print(12, false, out, dateFormat.format(ts));
else
print(10, false, out, ts / 1000);
out.print(' ');
print(7, true, out, appVersion != null ? appVersion.getNumClasses() : 0);
out.print(' ');
print(WIDTH, true, out, appVersion != null ? appVersion.getCodeSize() : 0);
for (int j = 0; j < TUPLE_SIZE; ++j) {
out.print(' ');
print(WIDTH, true, out, version.get(j));
}
out.println();
}
}
/** This is how dump() was implemented up to and including version 0.9.5. */
public void dumpXml(PrintStream out) {
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.println("<history>");
String startData = " <data ";
String stop = "/>";
for (int i = 0; i < versionList.length; ++i) {
Version version = versionList[i];
AppVersion appVersion = sequenceToAppVersionMap.get(version.getSequence());
out.print(" <historyItem ");
out.print("seq=\"");
out.print(i);
out.print("\" ");
out.print("version=\"");
out.print(appVersion != null ? appVersion.getReleaseName() : "");
out.print("\" ");
out.print("time=\"");
if (formatDates)
out.print((appVersion != null ? new Date(appVersion.getTimestamp()).toString() : ""));
else
out.print(appVersion != null ? appVersion.getTimestamp() : 0L);
out.print("\"");
out.println(">");
String attributeName[] = new String[TUPLE_SIZE];
attributeName[0] = "added";
attributeName[1] = "newCode";
attributeName[2] = "fixed";
attributeName[3] = "removed";
attributeName[4] = "retained";
attributeName[5] = "dead";
attributeName[6] = "active";
for (int j = 0; j < TUPLE_SIZE; ++j) {
// newCode and retained are already comprised within active
// so we skip tehm
if (j == 1 || j == 4) {
continue;
}
out.print(startData + " name=\"" + attributeName[j] + "\" value=\"");
out.print(version.get(j));
out.print("\"");
out.println(stop);
}
out.println(" </historyItem>");
}
out.print("</history>");
}
/**
* Get key used to classify the presence and/or abscence of a BugInstance in
* successive versions in the history.
*
* @param activePrevious
* true if the bug was active in the previous version, false if
* not
* @param activeCurrent
* true if the bug is active in the current version, false if not
* @return the key: one of ADDED, RETAINED, REMOVED, and DEAD
*/
private int getKey(boolean activePrevious, boolean activeCurrent) {
if (activePrevious)
return activeCurrent ? RETAINED : REMOVED;
else
// !activePrevious
return activeCurrent ? ADDED : DEAD;
}
class MineBugHistoryCommandLine extends CommandLine {
MineBugHistoryCommandLine() {
addSwitch("-formatDates", "render dates in textual form");
addSwitch("-noTabs", "delimit columns with groups of spaces for better alignment");
addSwitch("-xml", "output in XML format");
addSwitch("-summary", "just summarize changes over the last ten entries");
}
@Override
public void handleOption(String option, String optionalExtraPart) {
if (option.equals("-formatDates"))
setFormatDates(true);
else if (option.equals("-noTabs"))
setNoTabs();
else if (option.equals("-xml"))
setXml();
else if (option.equals("-summary"))
setSummary();
else
throw new IllegalArgumentException("unknown option: " + option);
}
@Override
public void handleOptionWithArgument(String option, String argument) {
throw new IllegalArgumentException("unknown option: " + option);
}
}
public static void main(String[] args) throws Exception {
FindBugs.setNoAnalysis();
DetectorFactoryCollection.instance(); // load plugins
MineBugHistory mineBugHistory = new MineBugHistory();
MineBugHistoryCommandLine commandLine = mineBugHistory.new MineBugHistoryCommandLine();
int argCount = commandLine.parse(args, 0, 2, "Usage: " + MineBugHistory.class.getName()
+ " [options] [<xml results> [<history]] ");
SortedBugCollection bugCollection = new SortedBugCollection();
if (argCount < args.length)
bugCollection.readXML(args[argCount++]);
else
bugCollection.readXML(System.in);
mineBugHistory.setBugCollection(bugCollection);
mineBugHistory.execute();
PrintStream out = System.out;
try {
if (argCount < args.length) {
out = UTF8.printStream(new FileOutputStream(args[argCount++]), true);
}
mineBugHistory.dump(out);
} finally {
out.close();
}
}
}