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    }