001    package org.hackystat.sensor.ant.pmd;
002    
003    import java.io.File;
004    import java.util.Date;
005    import java.util.HashMap;
006    import java.util.HashSet;
007    import java.util.List;
008    import java.util.Map;
009    import java.util.Set;
010    import java.util.Map.Entry;
011    
012    import javax.xml.bind.JAXBContext;
013    import javax.xml.bind.JAXBException;
014    import javax.xml.bind.Unmarshaller;
015    import javax.xml.datatype.XMLGregorianCalendar;
016    
017    import org.apache.tools.ant.BuildException;
018    import org.hackystat.sensor.ant.pmd.jaxb.ObjectFactory;
019    import org.hackystat.sensor.ant.pmd.jaxb.Pmd;
020    import org.hackystat.sensor.ant.pmd.jaxb.Violation;
021    import org.hackystat.sensor.ant.task.HackystatSensorTask;
022    import org.hackystat.sensor.ant.util.LongTimeConverter;
023    import org.hackystat.sensorshell.SensorShellException;
024    
025    /**
026     * Implements an Ant task that parses the XML files generated by PMD Ant Task and sends the
027     * CodeIssue data to a Hackystat server.
028     * 
029     * @author Aaron A. Kagawa, Julie Ann Sakuda, Philip Johnson
030     */
031    public class PmdSensor extends HackystatSensorTask {
032    
033      /** Prefix for type attributes. */
034      private static final String TYPE = "Type_";
035    
036      /** Tool in UserMap to use. */
037      private static String tool = "PMD";
038    
039      /** Initialize a new instance of a PmdSensor. */
040      public PmdSensor() {
041        super(tool);
042      }
043    
044      /**
045       * Initialize a new instance of a PmdSensor, passing the host email, and password directly. This
046       * supports testing.
047       * 
048       * @param host The hackystat host URL.
049       * @param email The Hackystat email to use.
050       * @param password The Hackystat password to use.
051       */
052      public PmdSensor(String host, String email, String password) {
053        super(host, email, password, tool);
054      }
055    
056      /**
057       * Parses the PMD XML files and sends the resulting code issue results to the hackystat server.
058       * This method is invoked automatically by Ant.
059       * 
060       * @throws BuildException If there is an error.
061       */
062      @Override
063      public void executeInternal() throws BuildException {
064        setupSensorShell();
065        int numberOfCodeIssues = 0;
066        Date startTime = new Date();
067    
068        // Iterate though each file, extract the PMD data, send to sensorshell.
069        for (File dataFile : getDataFiles()) {
070          verboseInfo("Processing file: " + dataFile);
071          try {
072            numberOfCodeIssues += processPmdXmlFile(dataFile);
073          }
074          catch (Exception e) {
075            signalError("Failure processing: " + dataFile, e);
076          }
077        }
078        this.sendAndQuit();
079        summaryInfo(startTime, "Code Issue", numberOfCodeIssues);
080      }
081    
082      /**
083       * Parses a PMD XML file and sends the code issue instances to the shell.
084       * 
085       * @param xmlFile The XML file name to be processed.
086       * @return The number of issues that have been processed in this XML file.
087       * @exception BuildException if any error.
088       */
089      public int processPmdXmlFile(File xmlFile) throws BuildException {
090        XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(this.runtime);
091    
092        try {
093          JAXBContext context = JAXBContext.newInstance(ObjectFactory.class);
094          Unmarshaller unmarshaller = context.createUnmarshaller();
095    
096          List<File> allSourceFiles = this.getSourceFiles();
097          Set<String> filesWithViolations = new HashSet<String>();
098    
099          Pmd pmdResults = (Pmd) unmarshaller.unmarshal(xmlFile);
100          List<org.hackystat.sensor.ant.pmd.jaxb.File> files = pmdResults.getFile();
101    
102          int codeIssueCount = 0;
103          verboseInfo("Processing information about files that had PMD issues.");
104          for (org.hackystat.sensor.ant.pmd.jaxb.File file : files) {
105            // Base unique timestamp off of the runtime (which is when it started running)
106            long uniqueTstamp = this.tstampSet.getUniqueTstamp(this.runtime);
107            // Get altered time as XMLGregorianCalendar
108            XMLGregorianCalendar uniqueTstampGregorian = LongTimeConverter
109                .convertLongToGregorian(uniqueTstamp);
110    
111            // derive the full path name from the file name
112            String fileName = file.getName();
113    
114            String fullFilePath = this.findSrcFile(allSourceFiles, fileName);
115            filesWithViolations.add(fullFilePath);
116    
117            Map<String, String> keyValMap = new HashMap<String, String>();
118            // Required
119            keyValMap.put("Tool", "PMD");
120            keyValMap.put("SensorDataType", "CodeIssue");
121            keyValMap.put("Timestamp", uniqueTstampGregorian.toString());
122            keyValMap.put("Runtime", runtimeGregorian.toString());
123            keyValMap.put("Resource", fullFilePath);
124    
125            HashMap<String, Integer> issueCounts = new HashMap<String, Integer>();
126    
127            List<Violation> violations = file.getViolation();
128            for (Violation violation : violations) {
129              String rule = violation.getRule();
130              String ruleset = violation.getRuleset();
131    
132              String key = ruleset + "_" + rule;
133              key = key.replaceAll(" ", ""); // remove spaces
134              if (issueCounts.containsKey(key)) {
135                Integer count = issueCounts.get(key);
136                issueCounts.put(key, ++count);
137              }
138              else {
139                // no previous mapping, add 1st issue to map
140                issueCounts.put(key, 1);
141              }
142            }
143            for (Entry<String, Integer> entry : issueCounts.entrySet()) {
144              String typeKey = TYPE + entry.getKey();
145              keyValMap.put(typeKey, entry.getValue().toString());
146            }
147    
148            this.sensorShell.add(keyValMap); // add data to sensorshell
149            codeIssueCount++;
150          }
151    
152          // process the zero issues
153          verboseInfo("Generating data for files that did not have PMD issues.");
154          for (File srcFile : getSourceFiles()) {
155            // Skip this entry if we've already processed it above. 
156            if (filesWithViolations.contains(srcFile.getAbsolutePath())) {
157              continue;
158            }
159            // Alter startTime to guarantee uniqueness.
160            long uniqueTstamp = this.tstampSet.getUniqueTstamp(this.runtime);
161    
162            // Get altered time as XMLGregorianCalendar
163            XMLGregorianCalendar uniqueTstampGregorian = LongTimeConverter
164                .convertLongToGregorian(uniqueTstamp);
165    
166            Map<String, String> keyValMap = new HashMap<String, String>();
167            keyValMap.put("Tool", "PMD");
168            keyValMap.put("SensorDataType", "CodeIssue");
169            keyValMap.put("Timestamp", uniqueTstampGregorian.toString());
170            keyValMap.put("Runtime", runtimeGregorian.toString());
171            keyValMap.put("Resource", srcFile.getAbsolutePath());
172    
173            this.sensorShell.add(keyValMap); // add data to sensorshell
174            codeIssueCount++;
175          }
176    
177          return codeIssueCount;
178        }
179        catch (JAXBException e) {
180          throw new BuildException(errMsgPrefix + "Failure in JAXB " + xmlFile, e);
181        }
182        catch (SensorShellException f) {
183          throw new BuildException(errMsgPrefix + "Failure in SensorShell " + xmlFile, f);
184        }
185      }
186    
187      /**
188       * Finds the full file path of the source path within the src files collection. For example,
189       * srcFiles could contain: [c:\foo\src\org\Foo.java, c:\foo\src\org\Bar.java] and the sourcePath
190       * could be org\Foo.java. This method will find and return the full path of the Foo.java file.
191       * 
192       * @param srcFiles Contains the full path to the files.
193       * @param sourcePath Contains a trimmed version of a file path.
194       * @return The full file path, or null if the path is not found.
195       */
196      private String findSrcFile(List<File> srcFiles, String sourcePath) {
197        for (File srcFile : srcFiles) {
198          if (srcFile == null) {
199            continue;
200          }
201          String srcFilePath = srcFile.getAbsolutePath();
202          String alteredSourcePath = sourcePath;
203          if (srcFilePath.contains("\\")) {
204            alteredSourcePath = sourcePath.replace('/', '\\');
205          }
206          if (srcFile != null && srcFilePath.contains(alteredSourcePath)) {
207            return srcFilePath;
208          }
209        }
210        return null;
211      }
212    }