001 package org.hackystat.telemetry.analyzer.reducer.impl; 002 003 import java.util.ArrayList; 004 import java.util.List; 005 006 import org.hackystat.dailyprojectdata.client.DailyProjectDataClient; 007 import org.hackystat.dailyprojectdata.resource.complexity.jaxb.ComplexityDailyProjectData; 008 import org.hackystat.dailyprojectdata.resource.complexity.jaxb.FileData; 009 import org.hackystat.sensorbase.resource.projects.jaxb.Project; 010 import org.hackystat.telemetry.analyzer.model.TelemetryDataPoint; 011 import org.hackystat.telemetry.analyzer.model.TelemetryStream; 012 import org.hackystat.telemetry.analyzer.model.TelemetryStreamCollection; 013 import org.hackystat.telemetry.analyzer.reducer.TelemetryReducer; 014 import org.hackystat.telemetry.analyzer.reducer.TelemetryReducerException; 015 import org.hackystat.telemetry.analyzer.reducer.util.IntervalUtility; 016 import org.hackystat.telemetry.service.server.ServerProperties; 017 import org.hackystat.utilities.time.interval.Interval; 018 import org.hackystat.utilities.time.period.Day; 019 import org.hackystat.utilities.tstamp.Tstamp; 020 021 022 /** 023 * Returns a single stream providing a Cyclomatic Complexity value. 024 * <p> 025 * Options: 026 * <ol> 027 * <li> mode: One of 'TotalMethods', 'TotalLines' 'TotalComplexity', 'AverageComplexityPerMethod', 028 * or 'TotalMethodsAboveComplexityThreshold'. Default is 'AverageComplexityPerMethod' 029 * <li> threshold: A string indicating the threshold value. Defaults to '10', which is the 030 * generally accepted threshold. This parameter is ignored unless the mode is 031 * 'TotalMethodsAboveComplexityThreshold', in which case it must be parsed to an integer. 032 * <li> tool: The tool whose sensor data is to be used to calculate the complexity value. 033 * Defaults to 'JavaNCSS'. 034 * </ol> 035 * 036 * @author Philip Johnson 037 */ 038 public class CyclomaticComplexityReducer implements TelemetryReducer { 039 040 /** Possible mode values. */ 041 public enum Mode { 042 /** Total methods. */ 043 TOTALMETHODS, 044 /** Total Lines. */ 045 TOTALLINES, 046 /** Total complexity. */ 047 TOTALCOMPLEXITY, 048 /** Average complexity per method. */ 049 AVERAGECOMPLEXITYPERMETHOD, 050 /** Total methods above complexity threshold. */ 051 TOTALMETHODSABOVECOMPLEXITYTHRESHOLD } 052 053 /** 054 * Computes and returns the required telemetry streams object. 055 * 056 * @param project The project. 057 * @param dpdClient The DPD Client. 058 * @param interval The interval. 059 * @param options The optional parameters. 060 * 061 * @return Telemetry stream collection. 062 * @throws TelemetryReducerException If there is any error. 063 */ 064 public TelemetryStreamCollection compute(Project project, DailyProjectDataClient dpdClient, 065 Interval interval, String[] options) throws TelemetryReducerException { 066 Mode mode = Mode.AVERAGECOMPLEXITYPERMETHOD; 067 String thresholdString = null; 068 String tool = "JavaNCSS"; 069 // process options 070 if (options.length > 3) { 071 throw new TelemetryReducerException("CyclomaticComplexity reducer needs only 3 parameters."); 072 } 073 if (options.length >= 1) { 074 try { 075 mode = Mode.valueOf(options[0].toUpperCase()); 076 } 077 catch (Exception e) { 078 throw new TelemetryReducerException("Illegal mode value: " + options[0], e); 079 } 080 } 081 082 if (options.length >= 2) { 083 thresholdString = options[1]; 084 } 085 086 if (options.length >= 3) { 087 tool = options[2]; 088 } 089 090 // Make sure threshold is a number if mode is TotalAboveThreshold. 091 int threshold = 10; 092 if (mode == Mode.TOTALMETHODSABOVECOMPLEXITYTHRESHOLD) { 093 try { 094 threshold = Integer.valueOf(thresholdString); 095 } 096 catch (Exception e) { 097 throw new TelemetryReducerException("Illegal threshold value: " + options[1], e); 098 } 099 } 100 101 102 // Find out the DailyProjectData host, throw error if not found. 103 String dpdHost = System.getProperty(ServerProperties.DAILYPROJECTDATA_FULLHOST_KEY); 104 if (dpdHost == null) { 105 throw new TelemetryReducerException("Null DPD host in CyclomaticComplexityReducer"); 106 } 107 108 // Now get the telemetry stream. 109 try { 110 TelemetryStream telemetryStream = this.getStream(dpdClient, project, interval, 111 mode, threshold, tool, null); 112 TelemetryStreamCollection streams = new TelemetryStreamCollection(null, project, interval); 113 streams.add(telemetryStream); 114 return streams; 115 } 116 catch (Exception e) { 117 throw new TelemetryReducerException(e); 118 } 119 } 120 121 /** 122 * Gets the telemetry stream. 123 * 124 * @param dpdClient The DailyProjectData client we will contact for the data. 125 * @param project The project. 126 * @param interval The interval. 127 * @param mode The mode. 128 * @param threshold The threshold (if mode is TOTALABOVETHRESHOLD). 129 * @param tool The tool whose sensor data will be used. 130 * @param streamTagValue The tag for the generated telemetry stream. 131 * 132 * @return The telemetry stream as required. 133 * 134 * @throws Exception If there is any error. 135 */ 136 TelemetryStream getStream(DailyProjectDataClient dpdClient, 137 Project project, Interval interval, Mode mode, 138 int threshold, String tool, Object streamTagValue) 139 throws Exception { 140 TelemetryStream telemetryStream = new TelemetryStream(streamTagValue); 141 List<IntervalUtility.Period> periods = IntervalUtility.getPeriods(interval); 142 143 for (IntervalUtility.Period period : periods) { 144 Double value = this.getData(dpdClient, project, period.getStartDay(), period.getEndDay(), 145 mode, threshold, tool); 146 telemetryStream.addDataPoint(new TelemetryDataPoint(period.getTimePeriod(), value)); 147 } 148 return telemetryStream; 149 } 150 151 /** 152 * Returns a Complexity value for the specified time interval, or null if no SensorData. 153 * 154 * We work backward through the time interval, and return a Complexity value for the first day in 155 * the interval for which Cyclomatic Complexity data exists. 156 * 157 * @param dpdClient The DailyProjectData client we will use to get this data. 158 * @param project The project. 159 * @param startDay The start day (inclusive). 160 * @param endDay The end day (inclusive). 161 * @param mode The mode. 162 * @param threshold The threshold, if mode is TOTALMETHODSABOVECOMPLEXITYTHRESHOLD. 163 * @param tool The tool whose sensor data will be used. 164 * @throws TelemetryReducerException If anything goes wrong. 165 * 166 * @return The Complexity value, or null if there is no Complexity SensorData in that period. 167 */ 168 Double getData(DailyProjectDataClient dpdClient, Project project, Day startDay, Day endDay, 169 Mode mode, int threshold, String tool) throws TelemetryReducerException { 170 try { 171 // Work backward through the interval, and return as soon as we get Complexity info. 172 for (Day day = endDay; day.compareTo(startDay) >= 0; day = day.inc(-1) ) { 173 // Get the DPD... 174 ComplexityDailyProjectData dpdData = 175 dpdClient.getComplexity(project.getOwner(), project.getName(), Tstamp.makeTimestamp(day), 176 "Cyclomatic", tool); 177 // Go to the next day in the interval if we don't have anything for this day. 178 if ((dpdData.getFileData() == null) || dpdData.getFileData().isEmpty()) { 179 continue; 180 } 181 182 // Otherwise we have complexity data, so calculate the desired values. 183 int numMethods = 0; 184 double totalComplexity = 0; 185 long totalLines = 0; 186 int totalAboveThreshold = 0; 187 for (FileData data : dpdData.getFileData()) { 188 totalLines += parseTotalLines(data.getTotalLines()); 189 List<Integer> complexities = parseList(data.getComplexityValues()); 190 for (Integer complexity : complexities) { 191 numMethods++; 192 totalComplexity += complexity; 193 if (complexity >= threshold) { 194 totalAboveThreshold++; 195 } 196 } 197 } 198 199 // Now return the value based upon mode. 200 switch (mode) { 201 case TOTALMETHODS: 202 return Double.valueOf(numMethods); 203 case TOTALCOMPLEXITY: 204 return Double.valueOf(totalComplexity); 205 case TOTALLINES: 206 return Double.valueOf(totalLines); 207 case AVERAGECOMPLEXITYPERMETHOD: 208 return Double.valueOf(totalComplexity / numMethods); 209 case TOTALMETHODSABOVECOMPLEXITYTHRESHOLD: 210 return Double.valueOf(totalAboveThreshold); 211 default: 212 throw new TelemetryReducerException("Unknown mode: " + mode); 213 } 214 } 215 } 216 catch (Exception ex) { 217 throw new TelemetryReducerException(ex); 218 } 219 // Never found appropriate complexity data in this interval, so return null. 220 return null; 221 } 222 223 /** 224 * Returns the comma-delimited list of complexity values as a list of integers. 225 * @param complexityList The comma-delimited list of complexity values. 226 * @return A list of integers. 227 * @throws TelemetryReducerException If errors occur parsing the list. 228 */ 229 private List<Integer> parseList(String complexityList) throws TelemetryReducerException { 230 List<Integer> complexities = new ArrayList<Integer>(); 231 String[] tokens = complexityList.split("\\s*,\\s*"); 232 for (String token : tokens) { 233 try { 234 int complexity = Integer.valueOf(token); 235 complexities.add(complexity); 236 } 237 catch (Exception e) { 238 throw new TelemetryReducerException("Error parsing complexity: '" + token + "'", e); 239 } 240 } 241 return complexities; 242 } 243 244 /** 245 * Takes a string representing totalLines, and returns it as a long. 246 * @param totalLinesString The total lines as a string. 247 * @return The value as a long. 248 * @throws TelemetryReducerException If errors during parsing. 249 */ 250 private long parseTotalLines(String totalLinesString) throws TelemetryReducerException { 251 long totalLines; 252 try { 253 totalLines = Long.valueOf(totalLinesString); 254 } 255 catch (Exception e) { 256 throw new TelemetryReducerException("Error parsing totalLines '" + totalLinesString + "'", e); 257 } 258 return totalLines; 259 } 260 }