/*
* FindBugs Eclipse Plug-in.
* Copyright (C) 2003 - 2004, Peter Friese
* 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 de.tobject.findbugs.builder;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import org.dom4j.DocumentException;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.preference.IPreferenceStore;
import de.tobject.findbugs.EclipseGuiCallback;
import de.tobject.findbugs.FindbugsPlugin;
import de.tobject.findbugs.io.IO;
import de.tobject.findbugs.marker.FindBugsMarker;
import de.tobject.findbugs.preferences.FindBugsConstants;
import de.tobject.findbugs.reporter.MarkerUtil;
import de.tobject.findbugs.reporter.Reporter;
import de.tobject.findbugs.util.Util.StopTimer;
import de.tobject.findbugs.view.FindBugsConsole;
import edu.umd.cs.findbugs.BugDesignation;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.FindBugs;
import edu.umd.cs.findbugs.FindBugs2;
import edu.umd.cs.findbugs.Project;
import edu.umd.cs.findbugs.SortedBugCollection;
import edu.umd.cs.findbugs.config.UserPreferences;
import edu.umd.cs.findbugs.workflow.Update;
/**
* Execute FindBugs on a collection of Java resources in a project.
*
* @author Peter Friese
* @author Andrei Loskutov
* @version 2.0
* @since 26.09.2003
*/
public class FindBugsWorker {
/** Controls debugging. */
public static boolean DEBUG;
private final IProgressMonitor monitor;
private final UserPreferences userPrefs;
private final IProject project;
private final IJavaProject javaProject;
private StopTimer st;
private final IResource resource;
public FindBugsWorker(IResource resource, IProgressMonitor monitor) throws CoreException {
super();
this.resource = resource;
this.project = resource.getProject();
this.javaProject = JavaCore.create(project);
if (javaProject == null || !javaProject.exists() || !javaProject.getProject().isOpen()) {
throw new CoreException(FindbugsPlugin.createErrorStatus("Java project is not open or does not exist: " + project,
null));
}
this.monitor = monitor;
// clone is required because we rewrite project relative references to absolute
this.userPrefs = FindbugsPlugin.getUserPreferences(project).clone();
}
/**
* Creates a new worker.
*
* @param project
* The <b>java</b> project to work on.
* @param monitor
* A progress monitor.
* @throws CoreException
* if the given project is not a java project, does not exists
* or is not open
*/
public FindBugsWorker(IProject project, IProgressMonitor monitor) throws CoreException {
this((IResource)project, monitor);
}
/**
* Run FindBugs on the given collection of resources from same project
* (note: This is currently not thread-safe)
*
* @param resources
* files or directories which should be on the project classpath.
* All resources must belong to the same project, and no one of
* the elements can contain another one. Ergo, if the list
* contains a project itself, then it must have only one element.
* @throws CoreException
*/
public void work(List<WorkItem> resources) throws CoreException {
if (resources == null || resources.isEmpty()) {
if (DEBUG) {
FindbugsPlugin.getDefault().logInfo("No resources to analyse for project " + project);
}
return;
}
if (DEBUG) {
System.out.println(resources);
}
st = new StopTimer();
st.newPoint("initPlugins");
// make sure it's initialized
FindbugsPlugin.applyCustomDetectors(false);
st.newPoint("clearMarkers");
// clear markers
clearMarkers(resources);
st.newPoint("configureOutputFiles");
final Project findBugsProject = new Project();
findBugsProject.setProjectName(javaProject.getElementName());
final Reporter bugReporter = new Reporter(javaProject, findBugsProject, monitor);
if (FindBugsConsole.getConsole() != null) {
bugReporter.setReportingStream(FindBugsConsole.getConsole().newOutputStream());
}
bugReporter.setPriorityThreshold(userPrefs.getUserDetectorThreshold());
FindBugs.setHome(FindbugsPlugin.getFindBugsEnginePluginLocation());
Map<IPath, IPath> outLocations = createOutputLocations();
// collect all related class/jar/war etc files for analysis
collectClassFiles(resources, outLocations, findBugsProject);
// attach source directories (can be used by some detectors, see
// SwitchFallthrough)
configureSourceDirectories(findBugsProject, outLocations);
if (findBugsProject.getFileCount() == 0) {
if (DEBUG) {
FindbugsPlugin.getDefault().logInfo("No resources to analyse for project " + project);
}
return;
}
st.newPoint("createAuxClasspath");
String[] classPathEntries = createAuxClasspath();
// add to findbugs classpath
for (String entry : classPathEntries) {
findBugsProject.addAuxClasspathEntry(entry);
}
String cloudId = userPrefs.getCloudId();
if (cloudId != null) {
findBugsProject.setCloudId(cloudId);
}
st.newPoint("configureProps");
IPreferenceStore store = FindbugsPlugin.getPluginPreferences(project);
boolean cacheClassData = store.getBoolean(FindBugsConstants.KEY_CACHE_CLASS_DATA);
final FindBugs2 findBugs = new FindBugs2Eclipse(project, cacheClassData, bugReporter);
findBugs.setNoClassOk(true);
findBugs.setProject(findBugsProject);
findBugs.setBugReporter(bugReporter);
findBugs.setProgressCallback(bugReporter);
findBugs.setDetectorFactoryCollection(DetectorFactoryCollection.instance());
// configure detectors.
userPrefs.setIncludeFilterFiles(relativeToAbsolute(userPrefs.getIncludeFilterFiles()));
userPrefs.setExcludeFilterFiles(relativeToAbsolute(userPrefs.getExcludeFilterFiles()));
userPrefs.setExcludeBugsFiles(relativeToAbsolute(userPrefs.getExcludeBugsFiles()));
findBugs.setUserPreferences(userPrefs);
// configure extended preferences
findBugs.setAnalysisFeatureSettings(userPrefs.getAnalysisFeatureSettings());
findBugs.setMergeSimilarWarnings(false);
if(cacheClassData) {
FindBugs2Eclipse.checkClassPathChanges(findBugs.getProject().getAuxClasspathEntryList(), project);
}
st.newPoint("runFindBugs");
if (DEBUG) {
FindbugsPlugin.log("Running findbugs");
}
runFindBugs(findBugs);
if (DEBUG) {
FindbugsPlugin.log("Done running findbugs");
}
// Merge new results into existing results
// if the argument is project, then it's not incremental
boolean incremental = !(resources.get(0) instanceof IProject);
updateBugCollection(findBugsProject, bugReporter, incremental);
st.newPoint("done");
st = null;
monitor.done();
}
private void configureSourceDirectories(Project findBugsProject, Map<IPath, IPath> outLocations) {
Set<IPath> srcDirs = outLocations.keySet();
for (IPath iPath : srcDirs) {
findBugsProject.addSourceDir(iPath.toOSString());
}
}
/**
* Load existing FindBugs xml report for the given collection of files.
*
* @param fileName
* xml file name to load bugs from
* @throws CoreException
*/
public void loadXml(String fileName) throws CoreException {
if (fileName == null) {
return;
}
st = new StopTimer();
// clear markers
clearMarkers(null);
final Project findBugsProject = new Project();
final Reporter bugReporter = new Reporter(javaProject, findBugsProject, monitor);
bugReporter.setPriorityThreshold(userPrefs.getUserDetectorThreshold());
reportFromXml(fileName, findBugsProject, bugReporter);
// Merge new results into existing results.
updateBugCollection(findBugsProject, bugReporter, false);
monitor.done();
}
/**
* Clear associated markers
*
* @param files
*/
private void clearMarkers(List<WorkItem> files) throws CoreException {
if (files == null) {
project.deleteMarkers(FindBugsMarker.NAME, true, IResource.DEPTH_INFINITE);
return;
}
for (WorkItem item : files) {
if (item != null) {
item.clearMarkers();
}
}
}
/**
* Updates given outputFiles map with class name patterns matching given
* java source names
*
* @param resources
* java sources
* @param outLocations
* key is src root, value is output location this directory
* @param fbProject
*/
private void collectClassFiles(List<WorkItem> resources, Map<IPath, IPath> outLocations, Project fbProject) {
for (WorkItem workItem : resources) {
workItem.addFilesToProject(fbProject, outLocations);
}
}
/**
* this method will block current thread until the findbugs is running
*
* @param findBugs
* fb engine, which will be <b>disposed</b> after the analysis is
* done
*/
private void runFindBugs(final FindBugs2 findBugs) {
if (DEBUG) {
FindbugsPlugin.log("Running findbugs in thread " + Thread.currentThread().getName());
}
System.setProperty("findbugs.progress", "true");
try {
// Perform the analysis! (note: This is not thread-safe)
findBugs.execute();
} catch (InterruptedException e) {
if (DEBUG) {
FindbugsPlugin.getDefault().logException(e, "Worker interrupted");
}
Thread.currentThread().interrupt();
} catch (IOException e) {
FindbugsPlugin.getDefault().logException(e, "Error performing FindBugs analysis");
} finally {
findBugs.dispose();
}
}
void logDirty(SortedBugCollection bugCollection) {
if (true) {
return;
}
int count = 0;
for(BugInstance b : bugCollection) {
BugDesignation bd = b.getUserDesignation();
if (bd == null) {
continue;
}
if (bd.isDirty()) {
count++;
}
}
if (count > 0) {
new RuntimeException("Found " + count + " dirty designations").printStackTrace(System.out);
}
}
/**
* Update the BugCollection for the project.
*
* @param findBugsProject
* FindBugs project representing analyzed classes
* @param bugReporter
* Reporter used to collect the new warnings
*/
private void updateBugCollection(Project findBugsProject, Reporter bugReporter, boolean incremental) {
SortedBugCollection newBugCollection = bugReporter.getBugCollection();
logDirty(newBugCollection);
try {
st.newPoint("getBugCollection");
SortedBugCollection oldBugCollection = FindbugsPlugin.getBugCollection(project, monitor, false);
logDirty(oldBugCollection);
st.newPoint("mergeBugCollections");
SortedBugCollection resultCollection = mergeBugCollections(oldBugCollection, newBugCollection, incremental);
logDirty(resultCollection);
resultCollection.getProject().setGuiCallback(new EclipseGuiCallback(project));
resultCollection.setTimestamp(System.currentTimeMillis());
resultCollection.setDoNotUseCloud(false);
resultCollection.reinitializeCloud();
logDirty(resultCollection);
// will store bugs in the default FB file + Eclipse project session
// props
st.newPoint("storeBugCollection");
FindbugsPlugin.storeBugCollection(project, resultCollection, monitor);
} catch (IOException e) {
FindbugsPlugin.getDefault().logException(e, "Error performing FindBugs results update");
} catch (CoreException e) {
FindbugsPlugin.getDefault().logException(e, "Error performing FindBugs results update");
}
// will store bugs as markers in Eclipse workspace
st.newPoint("createMarkers");
MarkerUtil.createMarkers(javaProject, newBugCollection, resource, monitor);
}
private SortedBugCollection mergeBugCollections(SortedBugCollection firstCollection, SortedBugCollection secondCollection,
boolean incremental) {
Update update = new Update();
// TODO copyDeadBugs must be true, otherwise incremental compile leads
// to
// unknown bug instances appearing (merged collection doesn't contain
// all bugs)
boolean copyDeadBugs = incremental;
SortedBugCollection merged = (SortedBugCollection) (update.mergeCollections(firstCollection, secondCollection,
copyDeadBugs, incremental));
return merged;
}
private Map<String, Boolean> relativeToAbsolute(Map<String, Boolean> map) {
Map<String, Boolean> resultMap = new TreeMap<String, Boolean>();
for (Entry<String, Boolean> entry : map.entrySet()) {
if(!entry.getValue().booleanValue()) {
continue;
}
String filePath = entry.getKey();
IPath path = getFilterPath(filePath, project);
if (!path.toFile().exists()) {
FindbugsPlugin.getDefault().logWarning("Filter not found: " + filePath);
continue;
}
String filterName = path.toOSString();
resultMap.put(filterName, Boolean.TRUE);
}
return resultMap;
}
/**
* Checks the given path and convert it to absolute path if it is specified
* relative to the given project or workspace
*
* @param filePath
* project relative OR workspace relative OR absolute OS file
* path (1.3.8+ version)
* @param project
* might be null (only for workspace relative or absolute paths)
* @return absolute path which matches given relative or absolute path,
* never null
*/
public static IPath getFilterPath(String filePath, IProject project) {
IPath path = new Path(filePath);
if (path.isAbsolute()) {
return path;
}
if (project != null) {
// try first project relative location
IPath newPath = project.getLocation().append(path);
if (newPath.toFile().exists()) {
return newPath;
}
}
// try to resolve relative to workspace (if we use workspace properties
// for project)
IPath wspLocation = ResourcesPlugin.getWorkspace().getRoot().getLocation();
IPath newPath = wspLocation.append(path);
if (newPath.toFile().exists()) {
return newPath;
}
// something which we have no idea what it can be (or missing/wrong file
// path)
return path;
}
/**
* Checks the given absolute path and convert it to relative path if it is
* relative to the given project or workspace. This representation can be
* used to store filter paths in user preferences file
*
* @param filePath
* absolute OS file path
* @param project
* might be null
* @return filter file path as stored in preferences which matches given
* path
*/
public static IPath toFilterPath(String filePath, IProject project) {
IPath path = new Path(filePath);
IPath commonPath;
if (project != null) {
commonPath = project.getLocation();
IPath relativePath = getRelativePath(path, commonPath);
if (!relativePath.equals(path)) {
return relativePath;
}
}
commonPath = ResourcesPlugin.getWorkspace().getRoot().getLocation();
return getRelativePath(path, commonPath);
}
/**
* @param filePath
* path (eventually absolute) to a file
* @param commonPath
* absolute path of some common location
* @return path relative to common root if given path is contained under the
* common directory, otherwise unchanged path
*/
private static IPath getRelativePath(IPath filePath, IPath commonPath) {
if (!filePath.isAbsolute()) {
return filePath;
}
// since Equinox 3.5 we can use IPath.makeRelativeTo(IPath)
//IPath relativeTo = filePath.makeRelativeTo(commonPath);
int matchingSegments = commonPath.matchingFirstSegments(filePath);
if (matchingSegments == commonPath.segmentCount()) {
// cut common prefix and discard device information
filePath = filePath.removeFirstSegments(matchingSegments).setDevice(null);
}
return filePath;
}
/**
* @return array with required class directories / libs on the classpath
*/
private String[] createAuxClasspath() {
return PDEClassPathGenerator.computeClassPath(javaProject);
}
/**
* @return map of all source folders to output folders, for current java
* project, where both are represented by absolute IPath objects
*
* @throws CoreException
*/
private Map<IPath, IPath> createOutputLocations() throws CoreException {
Map<IPath, IPath> srcToOutputMap = new HashMap<IPath, IPath>();
// get the default location => relative to wsp
IPath defaultOutputLocation = ResourceUtils.relativeToAbsolute(javaProject.getOutputLocation());
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
// path to the project without project name itself
IClasspathEntry entries[] = javaProject.getResolvedClasspath(true);
for (int i = 0; i < entries.length; i++) {
IClasspathEntry classpathEntry = entries[i];
if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
IPath outputLocation = ResourceUtils.getOutputLocation(classpathEntry, defaultOutputLocation);
if(outputLocation == null) {
continue;
}
IResource cpeResource = root.findMember(classpathEntry.getPath());
// patch from 2891041: do not analyze derived "source" folders
// because they probably contain auto-generated classes
if (cpeResource != null && cpeResource.isDerived()) {
continue;
}
// TODO not clear if it is absolute in workspace or in global FS
IPath srcLocation = ResourceUtils.relativeToAbsolute(classpathEntry.getPath());
if(srcLocation != null) {
srcToOutputMap.put(srcLocation, outputLocation);
}
}
}
return srcToOutputMap;
}
private void reportFromXml(final String xmlFileName, final Project findBugsProject, final Reporter bugReporter) {
if (!"".equals(xmlFileName)) {
FileInputStream input = null;
try {
input = new FileInputStream(xmlFileName);
bugReporter.reportBugsFromXml(input, findBugsProject);
} catch (FileNotFoundException e) {
FindbugsPlugin.getDefault().logException(e, "XML file not found: " + xmlFileName);
} catch (DocumentException e) {
FindbugsPlugin.getDefault().logException(e, "Invalid XML file: " + xmlFileName);
} catch (IOException e) {
FindbugsPlugin.getDefault().logException(e, "Error loading FindBugs results xml file: " + xmlFileName);
} finally {
IO.closeQuietly(input);
}
}
}
}