001    package org.hackystat.sensor.ant.emma;
002    
003    import java.io.File;
004    import java.util.Date;
005    import java.util.HashMap;
006    import java.util.Map;
007    
008    import javax.xml.bind.JAXBContext;
009    import javax.xml.bind.JAXBException;
010    import javax.xml.bind.Unmarshaller;
011    import javax.xml.datatype.XMLGregorianCalendar;
012    
013    import org.apache.tools.ant.BuildException;
014    import org.hackystat.sensor.ant.emma.jaxb.All;
015    import org.hackystat.sensor.ant.emma.jaxb.Class;
016    import org.hackystat.sensor.ant.emma.jaxb.Coverage;
017    import org.hackystat.sensor.ant.emma.jaxb.Data;
018    import org.hackystat.sensor.ant.emma.jaxb.Package;
019    import org.hackystat.sensor.ant.emma.jaxb.Report;
020    import org.hackystat.sensor.ant.emma.jaxb.Srcfile;
021    import org.hackystat.sensor.ant.task.HackystatSensorTask;
022    import org.hackystat.sensor.ant.util.JavaClass2FilePathMapper;
023    import org.hackystat.sensor.ant.util.LongTimeConverter;
024    import org.hackystat.sensorshell.SensorShellException;
025    
026    
027    /**
028     * Implements an Ant task that parses the XML files generated by Emma, a Java coverage tool.
029     * The Ant Task sends the Coverage data to a Hackystat server.
030     *
031     * @author Aaron A. Kagawa, Cedric Qin Zhang, Philip Johnson
032     */
033    public class EmmaSensor extends HackystatSensorTask {
034    
035      /** The mapper used to map class names to file paths. */
036      private JavaClass2FilePathMapper javaClass2FilePathMapper;
037      
038      /** The name of this tool. */
039      private static String tool = "Emma";
040      
041      /** Initialize a new instance of a EmmaSensor. */
042      public EmmaSensor() {
043        super(tool);
044      }
045    
046      /**
047       * Initialize a new instance of a EmmaSensor, passing the host and directory 
048       *   key in explicitly. This supports testing. Note that when this constructor 
049       *   is called, offline data recovery by the sensor is disabled.
050       * @param host The hackystat host URL.
051       * @param email The Hackystat email to use.
052       * @param password The Hackystat password to use.
053       */
054      public EmmaSensor(String host, String email, String password) {
055        super(host, email, password, tool);
056      }
057    
058      /**
059       * Parses the Coverage XML files and sends the resulting coverage results to
060       *   the hackystat server. This method is invoked automatically by Ant.
061       * @throws BuildException If there is an error.
062       */
063      @Override
064      public void executeInternal() throws BuildException {
065        this.setupSensorShell();
066        int numberOfEntries = 0;
067        Date startTime = new Date();
068        for (File dataFile : this.getDataFiles()) {
069          verboseInfo("Processing Emma file: " + dataFile);
070          try {
071            numberOfEntries += this.processCoverageXmlFile(dataFile);
072          }
073          catch (Exception e) {
074            signalError("Failure processing: " + dataFile, e);
075          }
076        }
077        this.sendAndQuit();
078        summaryInfo(startTime, "Coverage", numberOfEntries);
079      }
080    
081      /**
082       * Parses an Emma XML file and sends the data to the shell. The only coverage information that 
083       * is used by the sensor is the Emma class level report. All other coverage information is 
084       * ignored; for example the sensor does not use the method element coverage information. 
085       * Instead, the sensor parses the class element. Here is an example: 
086       * <pre>
087       * <class name="JUnitSensor">
088       *   <coverage type="class, %" value="100% (1/1)"/>
089       *   <coverage type="method, %" value="58%  (7/12)"/>
090       *   <coverage type="block, %" value="57%  (374/656)"/>
091       *   <coverage type="line, %" value="58%  (80.5/139)"/>
092       * &lt;/class&gt;
093       * </pre> 
094       * The granularities of class, method, block, and line are retrieved from the class element. 
095       * One could dig down into the method elements, but we are not doing this at the moment.  
096       * 
097       * @param xmlFile The XML file name to be processed.
098       * @return The number of coverage entries in this XML file.
099       */
100      public int processCoverageXmlFile(File xmlFile) {
101        XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(this.runtime);
102        // The start time for all entries will be approximated by the XML file's last mod time.
103        // The shell will ensure that it's unique by tweaking the millisecond field.
104        long startTime = xmlFile.lastModified();
105        try {
106          JAXBContext context = 
107            JAXBContext.newInstance(org.hackystat.sensor.ant.emma.jaxb.ObjectFactory.class);
108          Unmarshaller unmarshaller = context.createUnmarshaller();
109          
110          // emma report
111          Report report = (Report) unmarshaller.unmarshal(xmlFile);
112          Data data = report.getData();
113          All allData = data.getAll();
114          
115          int coverageEntriesCount = 0;
116          for (Package packageReport : allData.getPackage()) {
117            String packageName = packageReport.getName();
118            for (Srcfile srcfile : packageReport.getSrcfile()) {
119              for (Class classReport : srcfile.getClazz()) {
120                String className = classReport.getName();
121                String javaClassName = packageName + '.' + className;
122                String javaSourceFilePath = 
123                  this.getJavaClass2FilePathMapper().getFilePath(javaClassName);
124                if (javaSourceFilePath == null) {
125                  verboseInfo("Warning: Unable to find java source file path for class '" 
126                      + javaClassName + "'. Use empty string for file path.");
127                  javaSourceFilePath = "";
128                }
129                
130                // Alter startTime to guarantee uniqueness.
131                long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);
132    
133                // Get altered start time as XMLGregorianCalendar
134                XMLGregorianCalendar startTimeGregorian = 
135                  LongTimeConverter.convertLongToGregorian(uniqueTstamp);
136    
137                Map<String, String> keyValMap = new HashMap<String, String>();
138                keyValMap.put("Tool", "Emma");
139                keyValMap.put("SensorDataType", "Coverage");
140    
141                // Required
142                keyValMap.put("Runtime", runtimeGregorian.toString());
143                keyValMap.put("Timestamp", startTimeGregorian.toString());
144                keyValMap.put("Resource", javaSourceFilePath);
145    
146                // Optional
147                keyValMap.put("ClassName", javaClassName);
148                              
149                
150                for (Coverage coverage : classReport.getCoverage()) {
151                  String type = coverage.getType();
152                  String granularity = type.substring(0, type.indexOf(", %"));
153                  String value = coverage.getValue();
154                  String coveredString = value.substring(value.indexOf('(') + 1, value.indexOf('/'));
155                  String totalString = value.substring(value.indexOf('/') + 1, value.indexOf(')'));
156                  double covered = new Double(coveredString); 
157                  double total = new Double(totalString);
158    
159                  keyValMap.put(granularity  + "_Covered", String.valueOf(covered));
160                  keyValMap.put(granularity + "_Uncovered", String.valueOf(total - covered));
161                }
162                
163                this.sensorShell.add(keyValMap); // add data to sensorshell
164                coverageEntriesCount++;
165              }
166            }
167          }
168          return coverageEntriesCount;
169        }
170        catch (JAXBException e) {
171          throw new BuildException(errMsgPrefix + "JAXB Problem " + xmlFile, e);
172        }
173        catch (SensorShellException e) {
174          throw new BuildException(errMsgPrefix + "Sensor processing problem " + xmlFile, e);
175        }
176      }
177    
178      /**
179       * Get a java class to file path mapper.
180       * @return The mapper.
181       */
182      private JavaClass2FilePathMapper getJavaClass2FilePathMapper() {
183        if (this.javaClass2FilePathMapper == null) {
184          this.javaClass2FilePathMapper = new JavaClass2FilePathMapper(this.getSourceFiles());
185        }
186        return this.javaClass2FilePathMapper;
187      }
188    }