/* * FindBugs - Find Bugs in Java programs * Copyright (C) 2003-2008 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.cloud; import java.io.IOException; import java.io.PrintWriter; import java.net.URL; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import edu.umd.cs.findbugs.AppVersion; import edu.umd.cs.findbugs.BugCollection; import edu.umd.cs.findbugs.BugDesignation; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.BugInstance.XmlProps; import edu.umd.cs.findbugs.I18N; import edu.umd.cs.findbugs.IGuiCallback; import edu.umd.cs.findbugs.PackageStats; import edu.umd.cs.findbugs.ProjectStats; import edu.umd.cs.findbugs.PropertyBundle; import edu.umd.cs.findbugs.SourceLineAnnotation; import edu.umd.cs.findbugs.SystemProperties; import edu.umd.cs.findbugs.ba.AnalysisContext; import edu.umd.cs.findbugs.cloud.username.NameLookup; import edu.umd.cs.findbugs.util.ClassName; import edu.umd.cs.findbugs.util.Multiset; /** * @author William Pugh */ public abstract class AbstractCloud implements Cloud { public static long MIN_TIMESTAMP = new Date(96, 0, 23).getTime(); protected static final boolean THROW_EXCEPTION_IF_CANT_CONNECT = false; private static final Mode DEFAULT_VOTING_MODE = Mode.COMMUNAL; private static final Logger LOGGER = Logger.getLogger(AbstractCloud.class.getName()); private static final String LEADERBOARD_BLACKLIST = SystemProperties.getProperty("findbugs.leaderboard.blacklist"); private static final Pattern LEADERBOARD_BLACKLIST_PATTERN; static { Pattern p = null; if (LEADERBOARD_BLACKLIST != null) { try { p = Pattern.compile(LEADERBOARD_BLACKLIST.replace(',', '|')); } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not load leaderboard blacklist pattern", e); } } LEADERBOARD_BLACKLIST_PATTERN = p; } protected final CloudPlugin plugin; protected final BugCollection bugCollection; protected final PropertyBundle properties; @CheckForNull private Pattern sourceFileLinkPattern; private String sourceFileLinkFormat; private String sourceFileLinkFormatWithLine; private String sourceFileLinkToolTip; private final CopyOnWriteArraySet<CloudListener> listeners = new CopyOnWriteArraySet<CloudListener>(); private final CopyOnWriteArraySet<CloudStatusListener> statusListeners = new CopyOnWriteArraySet<CloudStatusListener>(); private Mode mode = Mode.COMMUNAL; private String statusMsg; private SigninState signinState = SigninState.UNAUTHENTICATED; protected AbstractCloud(CloudPlugin plugin, BugCollection bugs, Properties properties) { this.plugin = plugin; this.bugCollection = bugs; this.properties = plugin.getProperties().copy(); if (!properties.isEmpty()) { this.properties.loadProperties(properties); } } public boolean initialize() throws IOException { String modeString = getCloudProperty("votingmode"); Mode newMode = DEFAULT_VOTING_MODE; if (modeString != null) { try { newMode = Mode.valueOf(modeString.toUpperCase()); } catch (IllegalArgumentException e) { LOGGER.log(Level.WARNING, "No such voting mode " + modeString, e); } } setMode(newMode); String sp = properties.getProperty("findbugs.sourcelink.pattern"); String sf = properties.getProperty("findbugs.sourcelink.format"); String sfwl = properties.getProperty("findbugs.sourcelink.formatWithLine"); String stt = properties.getProperty("findbugs.sourcelink.tooltip"); if (sp != null && sf != null) { try { this.sourceFileLinkPattern = Pattern.compile(sp); this.sourceFileLinkFormat = sf; this.sourceFileLinkToolTip = stt; this.sourceFileLinkFormatWithLine = sfwl; } catch (RuntimeException e) { LOGGER.log(Level.WARNING, "Could not compile pattern " + sp, e); if (THROW_EXCEPTION_IF_CANT_CONNECT) throw e; } } return true; } public Mode getMode() { return mode; } public void setMode(Mode mode) { this.mode = mode; } public CloudPlugin getPlugin() { return plugin; } public BugCollection getBugCollection() { return bugCollection; } public boolean supportsBugLinks() { return false; } public void setBugLinkOnCloudAndStoreIssueDetails(BugInstance b, String viewUrl, String linkType) throws IOException, SignInCancelledException { throw new UnsupportedOperationException(); } public void updateBugStatusCache(BugInstance b, String status) { throw new UnsupportedOperationException(); } public boolean supportsClaims() { return false; } public boolean supportsCloudReports() { return true; } public String claimedBy(BugInstance b) { throw new UnsupportedOperationException(); } public boolean claim(BugInstance b) { throw new UnsupportedOperationException(); } public URL getBugLink(BugInstance b) { throw new UnsupportedOperationException(); } public String getBugLinkType(BugInstance instance) { return null; } public URL fileBug(BugInstance bug) { throw new UnsupportedOperationException(); } public BugFilingStatus getBugLinkStatus(BugInstance b) { throw new UnsupportedOperationException(); } public boolean canSeeCommentsByOthers(BugInstance bug) { switch (getMode()) { case SECRET: return false; case COMMUNAL: return true; case VOTING: return hasVoted(bug); } throw new IllegalStateException(); } public boolean hasVoted(BugInstance bug) { for (BugDesignation bd : getLatestDesignationFromEachUser(bug)) if (getUser().equals(bd.getUser())) return true; return false; } public String getCloudReport(BugInstance b) { return getSelectiveCloudReport(b, Collections.<String>emptySet()); } public String getCloudReportWithoutMe(BugInstance b) { String user = getUser(); Set<String> usersToExclude = user == null ? Collections.<String>emptySet() : Collections.singleton(user); return getSelectiveCloudReport(b, usersToExclude); } private String getSelectiveCloudReport(BugInstance b, Set<String> usersToExclude) { SimpleDateFormat format = new SimpleDateFormat("MM/dd, yyyy"); StringBuilder builder = new StringBuilder(); long firstSeen = getFirstSeen(b); builder.append(String.format("First seen %s%n", format.format(new Date(firstSeen)))); builder.append("\n"); I18N i18n = I18N.instance(); boolean canSeeCommentsByOthers = canSeeCommentsByOthers(b); if (canSeeCommentsByOthers && supportsBugLinks()) { BugFilingStatus bugLinkStatus = getBugLinkStatus(b); if (bugLinkStatus != null && bugLinkStatus.bugIsFiled()) { builder.append("\nBug status is ").append(getBugStatus(b)); if (getBugIsUnassigned(b)) builder.append("\nBug is unassigned"); builder.append("\n\n"); } } String me = getUser(); for (BugDesignation d : getLatestDesignationFromEachUser(b)) { if (!usersToExclude.contains(d.getUser()) && (me != null && me.equals(d.getUser()) || canSeeCommentsByOthers)) { builder.append(String.format("%s@ %s: %s%n", d.getUser() == null ? "" : d.getUser() + " ", format.format(new Date(d.getTimestamp())), i18n.getUserDesignation(d.getDesignationKey()))); String annotationText = d.getAnnotationText(); if (annotationText != null && annotationText.length() > 0) { builder.append(annotationText); builder.append("\n\n"); } } } return builder.toString(); } public String getBugStatus(BugInstance b) { return null; } protected abstract Iterable<BugDesignation> getLatestDesignationFromEachUser(BugInstance bd); public Date getUserDate(BugInstance b) { return new Date(getUserTimestamp(b)); } public void addListener(CloudListener listener) { if (listener == null) throw new NullPointerException(); if (!listeners.contains(listener)) listeners.add(listener); } public void removeListener(CloudListener listener) { listeners.remove(listener); } public void addStatusListener(CloudStatusListener listener) { if (listener == null) throw new NullPointerException(); if (!statusListeners.contains(listener)) statusListeners.add(listener); } public void removeStatusListener(CloudStatusListener listener) { statusListeners.remove(listener); } public String getStatusMsg() { return statusMsg; } public void shutdown() { } public boolean getIWillFix(BugInstance b) { return getUserDesignation(b) == UserDesignation.I_WILL_FIX; } public UserDesignation getConsensusDesignation(BugInstance b) { if (b == null) throw new NullPointerException("null bug instance"); Multiset<UserDesignation> designations = new Multiset<UserDesignation>(); int count = 0; int totalCount = 0; double total = 0.0; int isAProblem = 0; int notAProblem = 0; for (BugDesignation designation : getLatestDesignationFromEachUser(b)) { UserDesignation d = UserDesignation.valueOf(designation.getDesignationKey()); if (d == UserDesignation.I_WILL_FIX) d = UserDesignation.MUST_FIX; else if (d == UserDesignation.UNCLASSIFIED) continue; switch (d) { case I_WILL_FIX: case MUST_FIX: case SHOULD_FIX: isAProblem++; break; case BAD_ANALYSIS: case NOT_A_BUG: case MOSTLY_HARMLESS: case OBSOLETE_CODE: notAProblem++; break; } designations.add(d); totalCount++; if (d.nonVoting()) continue; count++; total += d.score(); } if (totalCount == 0) return UserDesignation.UNCLASSIFIED; UserDesignation mostCommonVotingDesignation = null; UserDesignation mostCommonDesignation = null; for (Map.Entry<UserDesignation, Integer> e : designations.entriesInDecreasingFrequency()) { UserDesignation d = e.getKey(); if (mostCommonVotingDesignation == null && !d.nonVoting()) { mostCommonVotingDesignation = d; if (e.getValue() > count / 2) return d; } if (mostCommonDesignation == null && d != UserDesignation.UNCLASSIFIED) { mostCommonDesignation = d; if (e.getValue() > count / 2) return d; } } double score = total / count; if (score >= UserDesignation.SHOULD_FIX.score() || isAProblem > notAProblem) return UserDesignation.SHOULD_FIX; if (score <= UserDesignation.NOT_A_BUG.score()) return UserDesignation.NOT_A_BUG; if (score <= UserDesignation.MOSTLY_HARMLESS.score() || notAProblem > isAProblem) return UserDesignation.MOSTLY_HARMLESS; return UserDesignation.NEEDS_STUDY; } public boolean overallClassificationIsNotAProblem(BugInstance b) { UserDesignation consensusDesignation = getConsensusDesignation(b); return consensusDesignation != UserDesignation.UNCLASSIFIED && consensusDesignation.score() < 0; } public double getClassificationScore(BugInstance b) { int count = 0; double total = 0.0; for (BugDesignation designation : getLatestDesignationFromEachUser(b)) { UserDesignation d = UserDesignation.valueOf(designation.getDesignationKey()); if (d.nonVoting()) continue; count++; total += d.score(); } return total / count; } public double getClassificationVariance(BugInstance b) { int count = 0; double total = 0.0; double totalSquares = 0.0; for (BugDesignation designation : getLatestDesignationFromEachUser(b)) { UserDesignation d = UserDesignation.valueOf(designation.getDesignationKey()); if (d.nonVoting()) continue; count++; total += d.score(); totalSquares += d.score() * d.score(); } double average = total / count; return totalSquares / count - average * average; } public double getPortionObsoleteClassifications(BugInstance b) { int count = 0; double total = 0.0; for (BugDesignation designation : getLatestDesignationFromEachUser(b)) { count++; UserDesignation d = UserDesignation.valueOf(designation.getDesignationKey()); if (d == UserDesignation.OBSOLETE_CODE) total++; } return total / count; } public int getNumberReviewers(BugInstance b) { int count = 0; Iterable<BugDesignation> designations = getLatestDesignationFromEachUser(b); // noinspection UnusedDeclaration for (BugDesignation designation : designations) { count++; } return count; } @SuppressWarnings("boxing") public void printCloudSummary(PrintWriter w, Iterable<BugInstance> bugs, String[] packagePrefixes) { Multiset<String> evaluations = new Multiset<String>(); Multiset<String> designations = new Multiset<String>(); Multiset<String> bugStatus = new Multiset<String>(); int issuesWithThisManyReviews[] = new int[100]; I18N i18n = I18N.instance(); int packageCount = 0; int classCount = 0; int ncss = 0; ProjectStats projectStats = bugCollection.getProjectStats(); for (PackageStats ps : projectStats.getPackageStats()) { int num = ps.getNumClasses(); if (ClassName.matchedPrefixes(packagePrefixes, ps.getPackageName()) && num > 0) { packageCount++; ncss += ps.size(); classCount += num; } } if (classCount == 0) { w.println("No classes were analyzed"); return; } if (packagePrefixes != null && packagePrefixes.length > 0) { String lst = Arrays.asList(packagePrefixes).toString(); w.println("Code analyzed in " + lst.substring(1, lst.length() - 1)); } else { w.println("Code analyzed"); } w.printf("%,7d packages%n%,7d classes%n", packageCount, classCount); if (ncss > 0) w.printf("%,7d thousands of lines of non-commenting source statements%n", (ncss + 999) / 1000); w.println(); int count = 0; for (BugInstance bd : bugs) { count++; HashSet<String> reviewers = new HashSet<String>(); String status = supportsBugLinks() && getBugLinkStatus(bd).bugIsFiled() ? getBugStatus(bd) : null; if (status != null) bugStatus.add(status); for (BugDesignation d : getLatestDesignationFromEachUser(bd)) if (reviewers.add(d.getUser())) { evaluations.add(d.getUser()); designations.add(i18n.getUserDesignation(d.getDesignationKey())); } int numReviews = Math.min(reviewers.size(), issuesWithThisManyReviews.length - 1); issuesWithThisManyReviews[numReviews]++; } if (count == getBugCollection().getCollection().size()) w.printf("Summary for %d issues%n%n", count); else w.printf("Summary for %d issues that are in the current view%n%n", count); if (evaluations.numKeys() == 0) { w.println("No reviews found"); } else { w.println("People who have performed the most reviews"); printLeaderBoard(w, evaluations, 9, getUser(), true, "reviewer"); w.println(); w.println("Distribution of reviews"); printLeaderBoard(w, designations, 100, " --- ", false, "designation"); } if (supportsBugLinks()) { if (bugStatus.numKeys() == 0) { w.println(); w.println("No bugs filed"); } else { w.println(); w.println("Distribution of bug status"); printLeaderBoard(w, bugStatus, 100, " --- ", false, "status of filed bug"); } } w.println(); w.println("Distribution of number of reviews"); for (int i = 0; i < issuesWithThisManyReviews.length; i++) if (issuesWithThisManyReviews[i] > 0) { w.printf("%4d with %3d review", issuesWithThisManyReviews[i], i); if (i != 1) w.print("s"); w.println(); } } @SuppressWarnings("boxing") public static void printLeaderBoard2(PrintWriter w, Multiset<String> evaluations, int maxRows, String alwaysPrint, String format, String title) { int row = 1; int position = 0; int previousScore = -1; boolean foundAlwaysPrint = false; for (Map.Entry<String, Integer> e : evaluations.entriesInDecreasingFrequency()) { int num = e.getValue(); if (num != previousScore) { position = row; previousScore = num; } String key = e.getKey(); if (LEADERBOARD_BLACKLIST_PATTERN != null && LEADERBOARD_BLACKLIST_PATTERN.matcher(key).matches()) continue; boolean shouldAlwaysPrint = key.equals(alwaysPrint); if (row <= maxRows || shouldAlwaysPrint) w.printf(format, position, num, key); if (shouldAlwaysPrint) foundAlwaysPrint = true; row++; if (row >= maxRows) { if (alwaysPrint == null) break; if (foundAlwaysPrint) { w.printf("Total of %d %ss%n", evaluations.numKeys(), title); break; } } } } public boolean supportsCloudSummaries() { return true; } public boolean canStoreUserAnnotation(BugInstance bugInstance) { return true; } public double getClassificationDisagreement(BugInstance b) { return 0; } public UserDesignation getUserDesignation(BugInstance b) { BugDesignation bd = getPrimaryDesignation(b); if (bd == null) return UserDesignation.UNCLASSIFIED; return UserDesignation.valueOf(bd.getDesignationKey()); } public String getUserEvaluation(BugInstance b) { BugDesignation bd = getPrimaryDesignation(b); if (bd == null) return ""; String result = bd.getAnnotationText(); if (result == null) return ""; return result; } public long getUserTimestamp(BugInstance b) { BugDesignation bd = getPrimaryDesignation(b); if (bd == null) return Long.MAX_VALUE; return bd.getTimestamp(); } public long getFirstSeen(BugInstance b) { return getLocalFirstSeen(b); } public void addDateSeen(BugInstance b, long when) { throw new UnsupportedOperationException(); } // ==================== end of public methods ================== protected void updatedStatus() { for (CloudListener listener : listeners) { try { listener.statusUpdated(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error executing callback " + listener, e); } } } public void updatedIssue(BugInstance bug) { for (CloudListener listener : listeners) { try { listener.issueUpdated(bug); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error executing callback " + listener, e); } } } protected void fireIssueDataDownloadedEvent() { for (CloudStatusListener statusListener : statusListeners) statusListener.handleIssueDataDownloadedEvent(); } public SigninState getSigninState() { return signinState; } @SuppressWarnings({ "ThrowableInstanceNeverThrown" }) protected void setSigninState(SigninState state) { SigninState oldState = this.signinState; if (oldState == state) return; LOGGER.log(Level.FINER, "State " + oldState + " -> " + state, new Throwable()); this.signinState = state; for (CloudStatusListener statusListener : statusListeners) statusListener.handleStateChange(oldState, state); } public BugInstance getBugByHash(String hash) { for (BugInstance instance : bugCollection.getCollection()) { if (instance.getInstanceHash().equals(hash)) { return instance; } } return null; } protected NameLookup getUsernameLookup() throws IOException { NameLookup lookup; try { lookup = plugin.getUsernameClass().newInstance(); } catch (Exception e) { throw new RuntimeException("Unable to obtain username", e); } if (!lookup.signIn(plugin, bugCollection)) { throw new RuntimeException("Unable to obtain username"); } return lookup; } public MutableCloudTask createTask(final String name) { MutableCloudTask task = new MutableCloudTask(name); for (CloudListener listener : listeners) { listener.taskStarted(task); } task.setDefaultListener(new CloudTaskListener() { public void taskStatusUpdated(String statusLine, double percentCompleted) { setStatusMsg(name + "... " + statusLine); } public void taskFinished() { setStatusMsg(""); } public void taskFailed(String message) { setStatusMsg(name + "... FAILED - " + message); } }); if (task.isUsingDefaultListener()) { setStatusMsg(name); } return task; } public void setStatusMsg(String newMsg) { this.statusMsg = newMsg; updatedStatus(); } private static void printLeaderBoard(PrintWriter w, Multiset<String> evaluations, int maxRows, String alwaysPrint, boolean listRank, String title) { if (listRank) w.printf("%3s %4s %s%n", "rnk", "num", title); else w.printf("%4s %s%n", "num", title); printLeaderBoard2(w, evaluations, maxRows, alwaysPrint, listRank ? "%3d %4d %s%n" : "%2$4d %3$s%n", title); } protected String getCloudProperty(String propertyName) { return properties.getProperty("findbugs.cloud." + propertyName); } public boolean supportsSourceLinks() { return sourceFileLinkPattern != null; } @SuppressWarnings("boxing") public @CheckForNull URL getSourceLink(BugInstance b) { if (sourceFileLinkPattern == null) return null; SourceLineAnnotation src = b.getPrimarySourceLineAnnotation(); String fileName = src.getSourcePath(); int startLine = src.getStartLine(); int endLine = src.getEndLine(); java.util.regex.Matcher m = sourceFileLinkPattern.matcher(fileName); boolean isMatch = m.matches(); if (isMatch) try { URL link; if (startLine > 0) link = new URL(String.format(sourceFileLinkFormatWithLine, m.group(1), startLine, startLine - 10, endLine)); else link = new URL(String.format(sourceFileLinkFormat, m.group(1))); return link; } catch (Exception e) { AnalysisContext.logError("Error generating source link for " + src, e); } return null; } public String getSourceLinkToolTip(BugInstance b) { return sourceFileLinkToolTip; } /* * (non-Javadoc) * * @see * edu.umd.cs.findbugs.cloud.Cloud#getBugIsUnassigned(edu.umd.cs.findbugs * .BugInstance) */ public boolean getBugIsUnassigned(BugInstance b) { return true; } /* * (non-Javadoc) * * @see * edu.umd.cs.findbugs.cloud.Cloud#getWillNotBeFixed(edu.umd.cs.findbugs * .BugInstance) */ public boolean getWillNotBeFixed(BugInstance b) { return false; } public Set<String> getReviewers(BugInstance b) { HashSet<String> result = new HashSet<String>(); for (BugDesignation d : getLatestDesignationFromEachUser(b)) result.add(d.getUser()); return result; } public IGuiCallback getGuiCallback() { return getBugCollection().getProject().getGuiCallback(); } public String getCloudName() { return getPlugin().getDescription(); } public boolean communicationInitiated() { return !isOnlineCloud(); } public long getLocalFirstSeen(BugInstance b) { long firstVersion = b.getFirstVersion(); AppVersion v = getBugCollection().getAppVersionFromSequenceNumber(firstVersion); if (v == null) return getBugCollection().getTimestamp(); long firstSeen = v.getTimestamp(); if (b.hasXmlProps()) { XmlProps props = b.getXmlProps(); if (firstSeen > props.getFirstSeen().getTime()) firstSeen = props.getFirstSeen().getTime(); } return firstSeen; } }