package hudson.plugins.findbugs.parser; // NOPMD
import edu.umd.cs.findbugs.BugAnnotation;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.Project;
import edu.umd.cs.findbugs.SortedBugCollection;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.ba.SourceFile;
import edu.umd.cs.findbugs.ba.SourceFinder;
import hudson.plugins.analysis.core.AnnotationParser;
import hudson.plugins.analysis.util.model.FileAnnotation;
import hudson.plugins.analysis.util.model.LineRange;
import hudson.plugins.analysis.util.model.Priority;
import hudson.plugins.findbugs.FindBugsMessages;
import hudson.util.IOUtils;
import org.apache.commons.digester.Digester;
import org.apache.commons.lang.StringUtils;
import org.dom4j.DocumentException;
import org.jvnet.localizer.LocaleProvider;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A parser for the native FindBugs XML files (ant task, batch file or
* maven-findbugs-plugin >= 1.2).
*
* @author Ulli Hafner
*/
// CHECKSTYLE:COUPLING-OFF
public class FindBugsParser implements AnnotationParser {
/** Unique ID of this class. */
private static final long serialVersionUID = 8306319007761954027L;
private static final String DOT = ".";
private static final String SLASH = "/";
private static final int HIGH_PRIORITY_LOWEST_RANK = 4;
private static final int NORMAL_PRIORITY_LOWEST_RANK = 9;
static {
DetectorFactoryCollection.rawInstance().setPluginList(new URL[0]);
}
/** Collection of source folders. */
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SE")
private final List<String> mavenSources = new ArrayList<String>();
/**
* Creates a new instance of {@link FindBugsParser}.
*/
public FindBugsParser() {
this(new ArrayList<String>());
}
/**
* Creates a new instance of {@link FindBugsParser}.
*
* @param sourceFolders
* a collection of folders to scan for source files. If empty,
* the source folders are guessed.
*/
public FindBugsParser(final Collection<String> sourceFolders) {
mavenSources.addAll(sourceFolders);
}
/** {@inheritDoc} */
public Collection<FileAnnotation> parse(final File file, final String moduleName) throws InvocationTargetException {
try {
Collection<String> sources = new ArrayList<String>(mavenSources);
if (sources.isEmpty()) {
String moduleRoot = StringUtils.substringBefore(file.getAbsolutePath().replace('\\', '/'), "/target/");
sources.add(moduleRoot + "/src/main/java");
sources.add(moduleRoot + "/src/test/java");
sources.add(moduleRoot + "/src");
}
return parse(file, sources, moduleName);
}
catch (IOException exception) {
throw new InvocationTargetException(exception);
}
catch (SAXException exception) {
throw new InvocationTargetException(exception);
}
catch (DocumentException exception) {
throw new InvocationTargetException(exception);
}
}
/**
* Returns the parsed FindBugs analysis file. This scanner accepts files in
* the native FindBugs format.
*
* @param file
* the FindBugs analysis file
* @param sources
* a collection of folders to scan for source files
* @param moduleName
* name of maven module
* @return the parsed result (stored in the module instance)
* @throws IOException
* if the file could not be parsed
* @throws DocumentException
* if the file could not be read
* @throws SAXException
* if the file could not be read
*/
public Collection<FileAnnotation> parse(final File file, final Collection<String> sources, final String moduleName)
throws IOException, DocumentException, SAXException {
FileInputStream input = null;
try {
input = new FileInputStream(file);
Map<String, String> hashToMessageMapping = createHashToMessageMapping(input);
IOUtils.closeQuietly(input);
input = new FileInputStream(file);
return parse(input, sources, moduleName, hashToMessageMapping);
}
finally {
IOUtils.closeQuietly(input);
}
}
/**
* Creates a mapping of FindBugs warnings to messages. A bug is represented
* by its unique hash code.
*
* @param file
* the FindBugs XML file
* @return the map of warning messages
* @throws SAXException
* if the file contains no valid XML
* @throws IOException
* signals that an I/O exception has occurred.
*/
public Map<String, String> createHashToMessageMapping(final InputStream file) throws SAXException, IOException {
Digester digester = new Digester();
digester.setValidating(false);
digester.setClassLoader(FindBugsParser.class.getClassLoader());
String rootXPath = "BugCollection/BugInstance";
digester.addObjectCreate(rootXPath, XmlBugInstance.class);
digester.addSetProperties(rootXPath);
String fileXPath = rootXPath + "/LongMessage";
digester.addCallMethod(fileXPath, "setMessage", 0);
digester.addSetNext(rootXPath, "add", Object.class.getName());
ArrayList<XmlBugInstance> bugs = new ArrayList<XmlBugInstance>();
digester.push(bugs);
digester.parse(file);
HashMap<String, String> mapping = new HashMap<String, String>();
for (XmlBugInstance bug : bugs) {
mapping.put(bug.getInstanceHash(), bug.getMessage());
}
return mapping;
}
/**
* Returns the parsed FindBugs analysis file. This scanner accepts files in
* the native FindBugs format.
*
* @param file
* the FindBugs analysis file
* @param sources
* a collection of folders to scan for source files
* @param moduleName
* name of maven module
* @param hashToMessageMapping
* mapping of hash codes to messages
* @return the parsed result (stored in the module instance)
* @throws IOException
* if the file could not be parsed
* @throws DocumentException in case of a parser exception
*/
public Collection<FileAnnotation> parse(final InputStream file, final Collection<String> sources,
final String moduleName, final Map<String, String> hashToMessageMapping) throws IOException,
DocumentException {
SortedBugCollection collection = new SortedBugCollection();
collection.readXML(file);
Project project = collection.getProject();
for (String sourceFolder : sources) {
project.addSourceDir(sourceFolder);
}
SourceFinder sourceFinder = new SourceFinder(project);
String actualName = extractModuleName(moduleName, project);
ArrayList<FileAnnotation> annotations = new ArrayList<FileAnnotation>();
Collection<BugInstance> bugs = collection.getCollection();
for (BugInstance warning : bugs) {
SourceLineAnnotation sourceLine = warning.getPrimarySourceLineAnnotation();
String message = warning.getMessage();
if (message.contains("TEST: Unknown")) {
message = FindBugsMessages.getInstance().getShortMessage(warning.getType(), LocaleProvider.getLocale());
}
Bug bug = new Bug(getPriority(warning),
StringUtils.defaultIfEmpty(hashToMessageMapping.get(warning.getInstanceHash()), message),
warning.getBugPattern().getCategory(),
warning.getType(), sourceLine.getStartLine(), sourceLine.getEndLine());
bug.setInstanceHash(warning.getInstanceHash());
long firstSeen = collection.getCloud().getFirstSeen(warning);
bug.setFirstSeen(firstSeen);
int ageInDays = (int) ((System.currentTimeMillis() - firstSeen) / 1000 / 60 / 60 / 24);
bug.setAgeInDays(ageInDays);
bug.setReviewCount(collection.getCloud().getNumberReviewers(warning));
boolean notAProblem = collection.getCloud().overallClassificationIsNotAProblem(warning);
if (notAProblem)
continue;
bug.setNotAProblem(notAProblem);
Iterator<BugAnnotation> annotationIterator = warning.annotationIterator();
while (annotationIterator.hasNext()) {
BugAnnotation bugAnnotation = annotationIterator.next();
if (bugAnnotation instanceof SourceLineAnnotation) {
SourceLineAnnotation annotation = (SourceLineAnnotation)bugAnnotation;
bug.addLineRange(new LineRange(annotation.getStartLine(), annotation.getEndLine()));
}
}
String fileName;
try {
SourceFile sourceFile = sourceFinder.findSourceFile(sourceLine);
fileName = sourceFile.getFullFileName();
}
catch (IOException exception) {
Logger.getLogger(getClass().getName()).log(Level.WARNING,
"Can't resolve absolute file name for file " + sourceLine.getSourceFile()
+ ", dir list = " + project.getSourceDirList().toString());
fileName = sourceLine.getPackageName().replace(DOT, SLASH) + SLASH + sourceLine.getSourceFile();
}
bug.setFileName(fileName);
bug.setPackageName(warning.getPrimaryClass().getPackageName());
bug.setModuleName(actualName);
annotations.add(bug);
}
return annotations;
}
/**
* Maps the FindBugs library rank to plug-in priority enumeration.
*
* @param warning
* the FindBugs warning
* @return mapped priority enumeration
*/
private Priority getPriority(final BugInstance warning) {
int rank = warning.getBugRank();
if (rank <= HIGH_PRIORITY_LOWEST_RANK)
return Priority.HIGH;
if (rank <= NORMAL_PRIORITY_LOWEST_RANK)
return Priority.NORMAL;
return Priority.LOW;
}
/**
* Extracts the module name from the specified project. If empty then the
* provided default name is used.
*
* @param defaultName
* the default module name to use
* @param project
* the maven 2 project
* @return the module name to use
*/
private String extractModuleName(final String defaultName, final Project project) {
if (StringUtils.isBlank(project.getProjectName())) {
return defaultName;
}
else {
return project.getProjectName();
}
}
}