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 * <file name="PmdSensor.java"> 085 * <class name="PmdSensor"> 086 * <metrics methods="18" conditionals="36" coveredstatements="77" coveredmethods="7" 087 * coveredconditionals="11" statements="141" coveredelements="95" elements="195"/> 088 * </class> 089 * <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"/> 092 * ... 093 * </file> 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 }