001    package org.hackystat.sensor.ant.clover;
002    
003    import java.util.Date;
004    import java.util.HashMap;
005    import java.util.Map;
006    
007    import javax.xml.bind.JAXBContext;
008    import javax.xml.bind.JAXBException;
009    import javax.xml.bind.Unmarshaller;
010    import javax.xml.datatype.XMLGregorianCalendar;
011    
012    import org.apache.tools.ant.BuildException;
013    import org.hackystat.sensor.ant.clover.jaxb.Coverage;
014    import org.hackystat.sensor.ant.clover.jaxb.File;
015    import org.hackystat.sensor.ant.clover.jaxb.Metrics;
016    import org.hackystat.sensor.ant.clover.jaxb.Package;
017    import org.hackystat.sensor.ant.clover.jaxb.Project;
018    import org.hackystat.sensor.ant.task.HackystatSensorTask;
019    import org.hackystat.sensor.ant.util.JavaClass2FilePathMapper;
020    import org.hackystat.sensor.ant.util.LongTimeConverter;
021    import org.hackystat.sensorshell.SensorShellException;
022    
023    /**
024     * Implements an Ant task that parses the XML files generated by the Clover coverage tool and 
025     * sends the data to a Hackystat server.
026     *
027     * @author Aaron A. Kagawa, Philip Johnson
028     */
029    public class CloverSensor extends HackystatSensorTask {
030    
031      /** The mapper used to map class names to file paths. */
032      private JavaClass2FilePathMapper javaClass2FilePathMapper;
033      
034      /** The name of this tool. */
035      private static String tool = "Clover";
036      
037      /** Initialize a new instance of a CloverSensor. */
038      public CloverSensor() {
039        super(tool);
040      }
041    
042      /**
043       * Initialize a new instance of a CloverSensor, passing the host and directory 
044       *   key in explicitly. This supports testing. Note that when this constructor 
045       *   is called, offline data recovery by the sensor is disabled.
046       * @param host The hackystat host URL.
047       * @param email The Hackystat email to use.
048       * @param password The Hackystat password to use.
049       */
050      public CloverSensor(String host, String email, String password) {
051        super(host, email, password, tool);
052      }
053    
054      /**
055       * Parses the Coverage XML files and sends the resulting coverage results to
056       * the hackystat server. This method is invoked automatically by Ant.
057       * @throws BuildException If there is an error.
058       */
059      @Override
060      public void executeInternal() throws BuildException {
061        this.setupSensorShell();
062        int numberOfEntries = 0;
063        Date startTime = new Date();
064        
065        for (java.io.File dataFile : getDataFiles()) {
066          verboseInfo("Processing Clover file: " + dataFile);
067          try {
068            numberOfEntries += this.processCoverageXmlFile(dataFile);
069          }
070          catch (Exception e) {
071            signalError("Failure processing Clover file: " + dataFile, e);
072          }
073        }
074        this.sendAndQuit();
075        summaryInfo(startTime, "Coverage", numberOfEntries);
076      }
077    
078      /**
079       * Parses an Clover XML file and sends the data to the shell. The only coverage information that 
080       * is used by the sensor is the Clover class level report. All other coverage information is 
081       * ignored; for example the sensor does not use the method element coverage information. 
082       * Instead, the sensor parses the class element. Here is an example: 
083       * <pre>
084       * &lt;file name="PmdSensor.java"&gt;
085       *   &lt;class name="PmdSensor"&gt;
086       *     &lt;metrics methods="18" conditionals="36" coveredstatements="77" coveredmethods="7" 
087       *       coveredconditionals="11" statements="141" coveredelements="95" elements="195"/&gt;
088       *   &lt;/class&gt;
089       *   &lt;metrics classes="1" methods="18" conditionals="36" ncloc="270" coveredstatements="77" 
090       *     coveredmethods="7" coveredconditionals="11" statements="141" loc="456" 
091       *     coveredelements="95" elements="195"/&gt;
092       *   ...
093       * &lt;/file&gt;
094       * </pre> 
095       * The granularities of file metrics element.  One could dig down into the line elements but 
096       * we aren't doing that now.   
097       * 
098       * @param xmlFile The XML file name to be processed.
099       * @exception BuildException if any error.
100       * @return The number of coverage entries in this XML file.
101       */
102      public int processCoverageXmlFile(java.io.File xmlFile) throws BuildException {
103        XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(this.runtime);
104        // The start time for all entries will be approximated by the XML file's last mod time.
105        // The shell will ensure that it's unique by tweaking the millisecond field.
106        long startTime = xmlFile.lastModified();
107    
108    
109        try {
110          JAXBContext context = 
111            JAXBContext.newInstance(org.hackystat.sensor.ant.clover.jaxb.ObjectFactory.class);
112          Unmarshaller unmarshaller = context.createUnmarshaller();
113          
114          // clover report
115          Coverage coverage = (Coverage) unmarshaller.unmarshal(xmlFile);
116          Project project = coverage.getProject();
117          
118          int coverageEntriesCount = 0;
119          for (Package packageReport : project.getPackage()) {
120            String packageName = packageReport.getName();
121            
122            for (File file : packageReport.getFile()) {
123              String fileName = file.getName();
124              Metrics metrics = file.getMetrics();
125              String className = file.getClazz().getName();
126              String javaClassName = packageName + '.' + className;
127              String javaSourceFilePath = fileName;
128              // for some reason sometimes file names can be fully qualified. 
129              // not sure how to configure clover to do that. if its not fully 
130              // qualified then we try to use the mapping. 
131              if (javaSourceFilePath.length() <= (className + ".java").length()) {
132                javaSourceFilePath = this.getJavaClass2FilePathMapper().getFilePath(javaClassName);
133                if (javaSourceFilePath == null) {
134                  verboseInfo("Warning: Unable to find java source file path for class '" 
135                      + javaClassName + "'. Using empty string as the resource.");
136                  javaSourceFilePath = "";
137                }
138              }
139              
140              // Alter startTime to guarantee uniqueness.
141              long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);
142    
143              // Get altered start time as XMLGregorianCalendar
144              XMLGregorianCalendar startTimeGregorian = 
145                LongTimeConverter.convertLongToGregorian(uniqueTstamp);
146    
147              Map<String, String> keyValMap = new HashMap<String, String>();
148              keyValMap.put("Tool", "Clover");
149              keyValMap.put("SensorDataType", "Coverage");
150    
151              // Required
152              keyValMap.put("Runtime", runtimeGregorian.toString());
153              keyValMap.put("Timestamp", startTimeGregorian.toString());
154              keyValMap.put("Resource", javaSourceFilePath);
155    
156              // Optional
157              keyValMap.put("ClassName", javaClassName);
158                            
159              int total = metrics.getConditionals();
160              int covered = metrics.getCoveredconditionals();
161              keyValMap.put("conditional_Covered", String.valueOf(covered));
162              keyValMap.put("conditional_Uncovered", String.valueOf(total - covered));
163              
164              total = metrics.getElements();
165              covered = metrics.getCoveredelements();
166              keyValMap.put("element_Covered", String.valueOf(covered));
167              keyValMap.put("element_Uncovered", String.valueOf(total - covered));
168    
169              total = metrics.getStatements();
170              covered = metrics.getCoveredstatements();
171              keyValMap.put("statement_Covered", String.valueOf(covered));
172              keyValMap.put("statement_Uncovered", String.valueOf(total - covered));
173    
174              total = metrics.getMethods();
175              covered = metrics.getCoveredmethods();
176              keyValMap.put("method_Covered", String.valueOf(covered));
177              keyValMap.put("method_Uncovered", String.valueOf(total - covered));
178    
179              // add data to sensorshell
180              this.sensorShell.add(keyValMap); 
181              coverageEntriesCount++;
182            }
183          }
184          return coverageEntriesCount;
185        }
186        catch (JAXBException e) {
187          throw new BuildException(errMsgPrefix + "Failure in JAXB " + xmlFile, e);
188        }
189        catch (SensorShellException f) {
190          throw new BuildException(errMsgPrefix + "Failure in SensorShell " + xmlFile, f);
191        }
192      }
193    
194      /**
195       * Get a java class to file path mapper.
196       * @return The mapper.
197       */
198      private JavaClass2FilePathMapper getJavaClass2FilePathMapper() {
199        if (this.javaClass2FilePathMapper == null) {
200          this.javaClass2FilePathMapper = new JavaClass2FilePathMapper(this.getSourceFiles());
201        }
202        return this.javaClass2FilePathMapper;
203      }
204      
205    }