001    package org.hackystat.sensor.ant.findbugs;
002    
003    import java.io.File;
004    import java.util.ArrayList;
005    import java.util.Date;
006    import java.util.HashMap;
007    import java.util.HashSet;
008    import java.util.List;
009    import java.util.Map;
010    import java.util.Set;
011    import java.util.Map.Entry;
012    
013    import javax.xml.bind.JAXBContext;
014    import javax.xml.bind.JAXBException;
015    import javax.xml.bind.Unmarshaller;
016    import javax.xml.datatype.XMLGregorianCalendar;
017    
018    import org.apache.tools.ant.BuildException;
019    import org.hackystat.sensor.ant.findbugs.jaxb.BugCollection;
020    import org.hackystat.sensor.ant.findbugs.jaxb.BugInstance;
021    import org.hackystat.sensor.ant.findbugs.jaxb.SourceLine;
022    import org.hackystat.sensor.ant.task.HackystatSensorTask;
023    import org.hackystat.sensor.ant.util.LongTimeConverter;
024    import org.hackystat.sensorshell.SensorShellException;
025    
026    /**
027     * Implements an Ant task that parses the XML files generated by FindBugs and sends the test case
028     * results to the Hackystat server.
029     * 
030     * @author Philip Johnson, Hongbing Kou, Joy Agustin, Julie Ann Sakuda, Aaron A. Kagawa
031     */
032    public class FindBugsSensor extends HackystatSensorTask {
033    
034      /** The name of this tool. */
035      private static String tool = "FindBugs";
036    
037      /** Initialize a new instance of a FindBugsSensor. */
038      public FindBugsSensor() {
039        super(tool);
040      }
041    
042      /**
043       * Initialize a new instance of a FindBugsSensor, passing the host email, and password directly.
044       * This supports testing. Note that when this constructor is called, offline data recovery by the
045       * sensor is disabled.
046       * 
047       * @param host The hackystat host URL.
048       * @param email The Hackystat email to use.
049       * @param password The Hackystat password to use.
050       */
051      public FindBugsSensor(String host, String email, String password) {
052        super(host, email, password, tool);
053      }
054    
055      /**
056       * Parses the FindBugs XML files and sends the resulting FindBugs test case results to the
057       * hackystat server. This method is invoked automatically by Ant.
058       * 
059       * @throws BuildException If there is an error.
060       */
061      @Override
062      public void executeInternal() throws BuildException {
063        setupSensorShell();
064        int numberOfTests = 0;
065        Date startTime = new Date();
066        // Iterate though each file, extract the FindBugs data, send to sensorshell.
067        for (File dataFile : getDataFiles()) {
068          try {
069            verboseInfo("Processing FindBugs file: " + dataFile);
070            numberOfTests += processFindBugsXmlFile(dataFile);
071          }
072          catch (Exception e) {
073            signalError("Failure processing: " + dataFile, e);
074          }
075        }
076        this.sendAndQuit();
077        summaryInfo(startTime, "CodeIssues", numberOfTests);
078      }
079    
080      /**
081       * Parses a FindBugs XML file and sends the FindBugsEntry instances to the shell.
082       * 
083       * @param xmlFile The XML file name to be processed.
084       * @exception BuildException if any error.
085       * @return The number of test cases in this XML file.
086       */
087      public int processFindBugsXmlFile(File xmlFile) throws BuildException {
088        XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(new Date()
089            .getTime());
090        // The start time for all entries will be approximated by the XML file's last mod time.
091        // The shell will ensure that it's unique by tweaking the millisecond field.
092        long startTime = xmlFile.lastModified();
093        try {
094          JAXBContext context = JAXBContext
095              .newInstance(org.hackystat.sensor.ant.findbugs.jaxb.ObjectFactory.class);
096          Unmarshaller unmarshaller = context.createUnmarshaller();
097    
098          BugCollection bugCollection = (BugCollection) unmarshaller.unmarshal(xmlFile);
099          Set<String> allSrcFiles = new HashSet<String>(bugCollection.getProject().getSrcDir());
100    
101          List<BugInstance> bugInstanceCollection = bugCollection.getBugInstance();
102    
103          // Sort all the bugs by the file they are from
104          HashMap<String, List<BugInstance>> fileToBugs = new HashMap<String, List<BugInstance>>();
105          for (BugInstance bugInstance : bugInstanceCollection) {
106            // Discard this bugInstance if we can't figure out the source path.
107            SourceLine sourceLine = bugInstance.getSourceLine();
108            if (sourceLine == null) {
109              continue;
110            }
111            String abstractSourcePath = sourceLine.getSourcepath();
112            if (abstractSourcePath == null) {
113              continue;
114            }
115            String fullSourcePath = this.findSrcFile(allSrcFiles, abstractSourcePath);
116            // Discard this bugInstance if we can't figure out the full source path.
117            if (fullSourcePath == null) {
118              continue;
119            }
120    
121            if (fileToBugs.containsKey(fullSourcePath)) {
122              List<BugInstance> bugs = fileToBugs.get(fullSourcePath);
123              bugs.add(bugInstance);
124            }
125            else {
126              List<BugInstance> bugs = new ArrayList<BugInstance>();
127              bugs.add(bugInstance);
128              fileToBugs.put(fullSourcePath, bugs);
129            }
130          }
131    
132          // now process all files with bugs
133          int codeIssueCount = 0;
134          for (Entry<String, List<BugInstance>> entry : fileToBugs.entrySet()) {
135            // Alter startTime to guarantee uniqueness.
136            long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);
137    
138            // Get altered start time as XMLGregorianCalendar
139            XMLGregorianCalendar timestamp = LongTimeConverter.convertLongToGregorian(uniqueTstamp);
140    
141            Map<String, String> keyValMap = new HashMap<String, String>();
142            keyValMap.put("Tool", "FindBugs");
143            keyValMap.put("SensorDataType", "CodeIssue");
144            keyValMap.put("Runtime", runtimeGregorian.toString());
145            keyValMap.put("Timestamp", timestamp.toString());
146            keyValMap.put("Resource", entry.getKey());
147    
148            HashMap<String, Integer> issueCounts = new HashMap<String, Integer>();
149            for (BugInstance bugInstance : entry.getValue()) {
150              String category = bugInstance.getCategory();
151              String type = bugInstance.getType();
152              String key = category + "_" + type;
153    
154              if (issueCounts.containsKey(key)) {
155                Integer count = issueCounts.get(key);
156                issueCounts.put(key, ++count);
157              }
158              else {
159                // no previous mapping, add 1st issue to map
160                issueCounts.put(key, 1);
161              }
162            }
163            for (Entry<String, Integer> issueCountEntry : issueCounts.entrySet()) {
164              String typeKey = "Type_" + issueCountEntry.getKey();
165              keyValMap.put(typeKey, issueCountEntry.getValue().toString());
166            }
167    
168            this.sensorShell.add(keyValMap);
169            codeIssueCount++;
170          }
171    
172          // process the zero issues
173          allSrcFiles.removeAll(fileToBugs.keySet());
174          for (String srcFile : allSrcFiles) {
175            // Alter startTime to guarantee uniqueness.
176            long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);
177    
178            // Get altered start time as XMLGregorianCalendar
179            XMLGregorianCalendar timestamp = LongTimeConverter.convertLongToGregorian(uniqueTstamp);
180    
181            Map<String, String> keyValMap = new HashMap<String, String>();
182            // Required
183            keyValMap.put("Tool", "FindBugs");
184            keyValMap.put("SensorDataType", "CodeIssue");
185            keyValMap.put("Runtime", runtimeGregorian.toString());
186            keyValMap.put("Timestamp", timestamp.toString());
187            keyValMap.put("Resource", srcFile);
188    
189            this.sensorShell.add(keyValMap); // add data to sensorshell
190            codeIssueCount++;
191          }
192          return codeIssueCount;
193        }
194        catch (JAXBException e) {
195          throw new BuildException(errMsgPrefix + "Failure in JAXB " + xmlFile, e);
196        }
197        catch (SensorShellException f) {
198          throw new BuildException(errMsgPrefix + "Failure in SensorShell " + xmlFile, f);
199        }
200      }
201    
202      /**
203       * Finds the full file path of the source path within the src files collection. For example,
204       * srcFiles could contain: [c:\foo\src\org\Foo.java, c:\foo\src\org\Bar.java] and the sourcePath
205       * could be org\Foo.java. This method will find and return the full path of the Foo.java file.
206       * 
207       * @param srcFiles Contains the full path to the files.
208       * @param sourcePath Contains a trimmed version of a file path.
209       * @return The full file path, or null if the path is not found.
210       */
211      private String findSrcFile(Set<String> srcFiles, String sourcePath) {
212        for (String srcFile : srcFiles) {
213          if (srcFile == null) {
214            continue;
215          }
216          String alteredSourcePath = sourcePath;
217          if (srcFile.contains("\\")) {
218            alteredSourcePath = sourcePath.replace('/', '\\');
219          }
220          if (srcFile != null && srcFile.contains(alteredSourcePath)) {
221            return srcFile;
222          }
223        }
224        return null;
225      }
226    }