View Javadoc

1   /**
2    * 
3    */
4   package sirius.utils.retriever;
5   
6   import java.io.File;
7   import java.io.FileInputStream;
8   import java.io.FileNotFoundException;
9   import java.io.IOException;
10  import java.util.Comparator;
11  import java.util.HashMap;
12  import java.util.LinkedHashMap;
13  import java.util.List;
14  import java.util.Locale;
15  import java.util.Map;
16  import java.util.SortedMap;
17  import java.util.TreeMap;
18  
19  import org.apache.maven.doxia.sink.Sink;
20  import org.apache.maven.doxia.siterenderer.Renderer;
21  import org.apache.maven.plugin.logging.Log;
22  import org.apache.maven.plugins.annotations.LifecyclePhase;
23  import org.apache.maven.plugins.annotations.Mojo;
24  import org.apache.maven.plugins.annotations.Parameter;
25  import org.apache.maven.project.MavenProject;
26  import org.apache.maven.reporting.AbstractMavenReport;
27  import org.apache.maven.reporting.MavenReportException;
28  
29  import sirius.utils.helpers.MapUtils;
30  import sirius.utils.retriever.types.usage.CucumberStep;
31  import sirius.utils.retriever.types.usage.CucumberStepDuration;
32  import sirius.utils.retriever.types.usage.CucumberStepSource;
33  
34  import com.cedarsoftware.util.io.JsonObject;
35  import com.cedarsoftware.util.io.JsonReader;
36  
37  /**
38   * Generates HTML report based on Cucumber usage page. The input is the result of the site:cucumber 
39   * goal which is the part of this plugin
40   * @author Myk Kolisnyk
41   * @goal cucumber-usage
42   * @phase site
43   */
44  public class CucumberUsageReportingPlugin extends AbstractMavenReport {
45  
46      /**
47       * The path to the JSON usage file which is an input for the report generation
48       * @parameter expression="${project.reporting.jsonUsageFile}"
49       * @required
50       */
51      private String       jsonUsageFile;
52  
53      /**
54       * The directory the output should be produced to.
55       * @parameter expression="${project.reporting.outputDirectory}"
56       * @required
57       * @readonly
58       */
59      private String       outputDirectory;
60  
61      /**
62       * @parameter default-value="${project}"
63       * @required
64       * @readonly
65       */
66      private MavenProject project;
67  
68      /**
69       * @component
70       * @required
71       * @readonly
72       */
73      private Renderer     siteRenderer;
74  
75      /*
76       * (non-Javadoc)
77       * 
78       * @see
79       * org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
80       */
81      @Override
82      public String getDescription(Locale arg0) {
83          return "HTML formatted Cucumber keywords usage report";
84      }
85  
86      /*
87       * (non-Javadoc)
88       * 
89       * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
90       */
91      @Override
92      public String getName(Locale arg0) {
93          return "Cucumber usage report";
94      }
95  
96      /*
97       * (non-Javadoc)
98       * 
99       * @see org.apache.maven.reporting.MavenReport#getOutputName()
100      */
101     @Override
102     public String getOutputName() {
103         return "cucumber-usage-report";
104     }
105 
106     /*
107      * (non-Javadoc)
108      * 
109      * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory()
110      */
111     @Override
112     protected String getOutputDirectory() {
113         return this.outputDirectory;
114     }
115 
116     /*
117      * (non-Javadoc)
118      * 
119      * @see org.apache.maven.reporting.AbstractMavenReport#getProject()
120      */
121     @Override
122     protected MavenProject getProject() {
123         return this.project;
124     }
125 
126     /*
127      * (non-Javadoc)
128      * 
129      * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer()
130      */
131     @Override
132     protected Renderer getSiteRenderer() {
133         return this.siteRenderer;
134     }
135 
136     /**
137      * @return the jsonUsageFile
138      */
139     public String getJsonUsageFile() {
140         return jsonUsageFile;
141     }
142 
143     /**
144      * @param jsonUsageFile
145      *            the jsonUsageFile to set
146      */
147     public void setJsonUsageFile(String jsonUsageFile) {
148         this.jsonUsageFile = jsonUsageFile;
149     }
150 
151     /**
152      * @param outputDirectory
153      *            the outputDirectory to set
154      */
155     public void setOutputDirectory(String outputDirectory) {
156         this.outputDirectory = outputDirectory;
157     }
158 
159     /**
160      * @param project
161      *            the project to set
162      */
163     public void setProject(MavenProject project) {
164         this.project = project;
165     }
166 
167     /**
168      * @param siteRenderer
169      *            the siteRenderer to set
170      */
171     public void setSiteRenderer(Renderer siteRenderer) {
172         this.siteRenderer = siteRenderer;
173     }
174 
175     public LinkedHashMap<String,Integer> calculateStepsUsageScore(CucumberStepSource[] sources){
176         LinkedHashMap<String,Integer> map = new LinkedHashMap<String,Integer>();
177         
178         for(CucumberStepSource source:sources){
179             int totalSteps = 0;
180             for(CucumberStep step:source.getSteps()){
181                 totalSteps += step.getDurations().length;
182             }
183             map.put(source.getSource(), totalSteps);
184         }
185         
186         map = (LinkedHashMap<String,Integer>)MapUtils.sortByValue(map);
187         
188         return map;
189     }
190     
191     public SortedMap calculateStepsUsageCounts(CucumberStepSource[] sources){
192         SortedMap<Integer,Integer> map = new TreeMap<Integer,Integer>();
193         for(CucumberStepSource source:sources){
194             int stepsCount = 0;//source.getSteps().length;
195             
196             for(CucumberStep step:source.getSteps()){
197                 stepsCount += step.getDurations().length;
198             }
199             
200             if(!map.containsKey(stepsCount)){
201                 map.put(stepsCount, 1);
202             }
203             else {
204                 int prevNum = map.get(stepsCount);
205                 prevNum++;
206                 map.remove(stepsCount);
207                 map.put(stepsCount, prevNum);
208             }
209         }
210         return map;
211     }
212     
213     public double calculateStepsUsageAverage(SortedMap<Integer,Integer> statistics){
214         int totalSteps = 0;
215         int totalUniqueSteps = 0;
216         
217         for(int i:statistics.keySet()){
218             totalSteps += i*statistics.get(i);
219             totalUniqueSteps += statistics.get(i);
220         }
221         if(totalUniqueSteps==0){
222             totalUniqueSteps=1;
223         }
224         return (double)totalSteps/(double)totalUniqueSteps;
225     }
226     
227     public int calculateStepsUsageMedian(SortedMap<Integer,Integer> statistics){
228         int totalSteps = 0;
229         int usedSteps = 0;
230         int median = 0;
231         for(int i:statistics.keySet()){
232             totalSteps += statistics.get(i);
233         }
234         
235         for(int i:statistics.keySet()){
236             usedSteps += statistics.get(i);
237             if(usedSteps*2 >= totalSteps){
238                 median=i;
239                 break;
240             }
241         }
242         
243         return median;
244     }
245     
246     public int calculateTotalSteps(SortedMap<Integer,Integer> statistics){
247         int totalSteps = 0;
248 
249         for(int i:statistics.keySet()){
250             totalSteps += i*statistics.get(i);
251         }
252         return totalSteps;
253     }
254  
255     public int calculateUsedSteps(SortedMap<Integer,Integer> statistics){
256         int usedSteps = 0;
257         
258         for(int i:statistics.keySet()){
259             usedSteps += statistics.get(i);
260         }
261         
262         return usedSteps;
263     }
264     
265     public int calculateStepsUsageMax(SortedMap<Integer,Integer> statistics){
266         int max=0;
267         for(int i:statistics.keySet()){
268             max = Math.max(max, statistics.get(i));
269         }
270         return max;
271     }
272     
273     /**
274      * .
275      * @param sink .
276      * @param sources .
277      */
278     protected void generateUsageOverviewGraphReport(Sink sink, CucumberStepSource[] sources){
279         double hscale;
280         double vscale;
281         int hsize = 400;
282         int vsize = 400;
283         int hstart = 40;
284         int vstart = 30;
285         int hend = 350;
286         int vend = 300;
287         
288         int hstep = 0;
289         int vstep=0;
290         
291         int median;
292         double average;
293         
294         SortedMap<Integer,Integer> map = calculateStepsUsageCounts(sources);
295         hscale = (double)(hend-2*hstart)/((double)map.lastKey()+1);
296         vscale = (double)(vend-2*vstart)/((double)calculateStepsUsageMax(map)+1);
297         
298         hstep = (int)(30./hscale)+1;
299         vstep = (int)(30./vscale)+1;
300         
301         median = calculateStepsUsageMedian(map);
302         average = calculateStepsUsageAverage(map);
303         
304         String htmlContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"" + (hsize+100) + "\" height=\"" + vsize + "\">" +
305         		"<defs>" +
306         		"<filter id=\"f1\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">" +
307         		"<feOffset result=\"offOut\" in=\"SourceAlpha\" dx=\"10\" dy=\"10\" />" +
308         		"<feGaussianBlur result=\"blurOut\" in=\"offOut\" stdDeviation=\"10\" />" +
309         		"<feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />" +
310         		"</filter>" +
311         		"<radialGradient id=\"grad1\" cx=\"0%\" cy=\"100%\" r=\"150%\" fx=\"0%\" fy=\"100%\">" +
312         		"<stop offset=\"0%\" style=\"stop-color:white;stop-opacity:0.1\" />" +
313         		"<stop offset=\"100%\" style=\"stop-color:gold;stop-opacity:0.7\" />" +
314         		"</radialGradient>" +
315         	    "<linearGradient id=\"grad2\" cx=\"0%\" cy=\"100%\" r=\"150%\" fx=\"0%\" fy=\"100%\">" +
316                 "<stop offset=\"0%\" style=\"stop-color:red;stop-opacity:0.7\" />" +
317                 "<stop offset=\"50%\" style=\"stop-color:yellow;stop-opacity:0.7\" />" +
318                 "<stop offset=\"100%\" style=\"stop-color:green;stop-opacity:0.7\" />" +
319                 "</linearGradient>" +
320         		"</defs>" +
321         		"<rect width=\"90%\" height=\"90%\" stroke=\"black\" " +
322         		  "stroke-width=\"1\" fill=\"url(#grad1)\" filter=\"url(#f1)\" />" +
323         		"<line x1=\"" + (hstart) + "\" y1=\"" + (vstart) + "\" x2=\"" + (hstart) + "\" y2=\"" + (vend) + "\" style=\"stroke:black;stroke-width:1\" />" +
324         		"<line x1=\"" + (hstart) + "\" y1=\"" + (vend) + "\" x2=\"" + (hend) + "\" y2=\"" + (vend) + "\" style=\"stroke:black;stroke-width:1\" />" +
325         		"<polygon points=\""+ (hstart-5) +"," + (vstart+20) + " "+ (hstart) +"," + (vstart) + " "+ (hstart+5) +"," + (vstart+20) + "\" style=\"fill:black;stroke:black;stroke-width:1\"/>" +
326         		"<polygon points=\""+(hend)+"," + (vend) + " "+(hend-20)+"," + (vend+5) + " "+(hend-20)+"," + (vend-5) + "\" style=\"fill:black;stroke:black;stroke-width:1\"/>" +
327         		"<polygon points=\"" + hstart + "," + vend;
328         for(int i=0;i<=map.lastKey()+1;i++){
329             int value = 0;
330             if(map.containsKey(i)){
331                 value = map.get(i);
332             }
333             htmlContent += " " + (hstart + (int)(i*hscale)) + "," + (vend - (int)(value*vscale));
334         }
335         htmlContent +=  "\" style=\"stroke:black;stroke-width:1\"  fill=\"url(#grad2)\" />";
336         
337         for(int i=0;i<=map.lastKey();i+=hstep){
338             htmlContent += "<line x1=\"" + (hstart + (int)(i*hscale)) + "\" y1=\""+ (vend)+ "\" x2=\"" + (hstart + (int)(i*hscale)) + "\" y2=\""+ (vend+5)+ "\" style=\"stroke:black;stroke-width:1\" />" +
339                     "<text x=\"" + (hstart + (int)(i*hscale)) + "\" y=\""+ (vend+10)+ "\" font-size = \"8\">" + i + "</text>";    
340         }
341         
342         for(int i=0;i<=calculateStepsUsageMax(map);i+=vstep){
343             htmlContent += "<line x1=\"" + (hstart) + "\" y1=\""+ (vend-(int)(i*vscale))+ "\" x2=\"" + (hstart - 5) + "\" y2=\""+ (vend-(int)(i*vscale))+ "\" style=\"stroke:black;stroke-width:1\" />" +
344                     "<text x=\"" + (hstart - 5) + "\" y=\""+ (vend-(int)(i*vscale))+ "\" transform=\"rotate(-90 " + (hstart - 5) + ","+ (vend-(int)(i*vscale))+ ")\" font-size = \"8\">" + i + "</text>";    
345         }
346                 
347         float usage = 100.f * (1.f - ((float)calculateUsedSteps(map)/ (float)calculateTotalSteps(map)));
348         String statusColor = "silver";
349         
350         if(usage <= 30.f){
351             statusColor = "red";
352         }
353         else if(usage >= 70){
354             statusColor = "green";
355         }
356         else {
357             statusColor = "#BBBB00";
358         }
359         
360         htmlContent += "<line stroke-dasharray=\"10,10\" x1=\"" + (hstart + median * hscale) + "\" y1=\"" + (vstart) + "\" x2=\"" + (hstart + median * hscale) + "\" y2=\"" + vend + "\" style=\"stroke:yellow;stroke-width:3\" />" +
361                 "<line stroke-dasharray=\"10,10\" x1=\"" + (hstart + (int)(average * hscale)) + "\" y1=\"" + (vstart) + "\" x2=\"" + (hstart + (int)(average * hscale)) + "\" y2=\"" + vend + "\" style=\"stroke:red;stroke-width:3\" />" +
362                 "<rect x=\"60%\" y=\"20%\" width=\"28%\" height=\"20%\" stroke=\"black\" stroke-width=\"1\" fill=\"white\" filter=\"url(#f1)\" />" +
363                 "<line x1=\"63%\" y1=\"29%\" x2=\"68%\" y2=\"29%\" stroke-dasharray=\"5,5\" style=\"stroke:red;stroke-width:3\" /><text x=\"73%\" y=\"30%\" font-weight = \"bold\" font-size = \"12\">Average</text>" +
364                 "<line x1=\"63%\" y1=\"34%\" x2=\"68%\" y2=\"34%\" stroke-dasharray=\"5,5\" style=\"stroke:yellow;stroke-width:3\" /><text x=\"73%\" y=\"35%\" font-weight = \"bold\" font-size = \"12\">Median</text>" +
365                 "<text x=\"60%\" y=\"55%\" font-weight = \"bold\" font-size = \"40\" fill=\"" + statusColor + "\">" + String.format("%.1f", usage)+ "%</text>" +
366                 "<text x=\"66%\" y=\"60%\" font-weight = \"bold\" font-size = \"16\" fill=\"" + statusColor + "\">Re-use</text>" +
367                 "<text x=\"120\" y=\"330\" font-weight = \"bold\" font-size = \"14\" >Step re-use count</text>" +
368                 "<text x=\"20\" y=\"220\" font-weight = \"bold\" font-size = \"14\" transform=\"rotate(-90 20,220)\">Steps count</text>" +
369                 "</svg>";
370         sink.rawText(htmlContent);
371     }
372     
373     /**
374      * .
375      * @param sink .
376      * @param sources .
377      */
378     protected void generateUsageOverviewTableReport(Sink sink, CucumberStepSource[] sources){
379         LinkedHashMap<String,Integer> map = calculateStepsUsageScore(sources);
380         
381         sink.table();
382         sink.tableRow();
383         sink.tableHeaderCell();
384         sink.text("Expression");
385         sink.tableHeaderCell_();
386         sink.tableHeaderCell();
387         sink.text("Occurences");
388         sink.tableHeaderCell_();
389         sink.tableRow_();
390         
391         for(String key:map.keySet()){
392             sink.tableRow();
393             sink.tableCell("80%");
394             sink.text(key);
395             sink.tableCell_();
396             sink.tableCell();
397             sink.text(""+map.get(key));
398             sink.tableCell_();
399             sink.tableRow_();
400         }
401         /*
402         for(CucumberStepSource source:sources){
403             sink.tableRow();
404             sink.tableCell("80%");
405             sink.text(source.getSource());
406             sink.tableCell_();
407             sink.tableCell();
408             int totalSteps = 0;
409             for(CucumberStep step:source.getSteps()){
410                 totalSteps += step.getDurations().length;
411             }
412             sink.text("" + totalSteps);
413             sink.tableCell_();
414             sink.tableRow_();
415         }*/
416         sink.table_();
417     }
418     
419     /**
420      * .
421      * @param sink .
422      * @param sources .
423      */
424     protected void generateUsageDetailedReport(Sink sink, CucumberStepSource[] sources){
425         for(CucumberStepSource source:sources){
426             sink.section3();
427             sink.sectionTitle3();
428             sink.text(source.getSource());
429             sink.sectionTitle3_();
430             sink.section3_();
431 
432             sink.table();
433             sink.tableRow();
434             sink.tableHeaderCell();
435             sink.text("Step Name");
436             sink.tableHeaderCell_();
437             sink.tableHeaderCell();
438             sink.text("Duration");
439             sink.tableHeaderCell_();
440             sink.tableHeaderCell();
441             sink.text("Location");
442             sink.tableHeaderCell_();
443             sink.tableRow_();
444             
445             for(CucumberStep step:source.getSteps()){
446                 sink.tableRow();
447                 sink.tableCell();
448                 sink.text(step.getName());
449                 sink.tableCell_();
450                 sink.tableCell();
451                 //sink.text("" + step.getAggregatedDurations().getAverage());
452                 sink.text("-");
453                 sink.tableCell_();
454                 sink.tableCell();
455                 //sink.text("" + step.getAggregatedDurations().getMedian());
456                 sink.text("-");
457                 sink.tableCell_();
458                 sink.tableRow_();
459                 for(CucumberStepDuration duration:step.getDurations()){
460                     sink.tableRow();
461                     sink.tableCell();
462                     sink.text("");
463                     sink.tableCell_();
464                     sink.tableCell();
465                     sink.text("" + duration.getDuration());
466                     sink.tableCell_();
467                     sink.tableCell();
468                     sink.text(duration.getLocation());
469                     sink.tableCell_();
470                     sink.tableRow_();
471                 }
472             }
473             sink.table_();
474         }
475 
476     }
477     
478     public CucumberStepSource[] getStepSources(String filePath) throws Exception{
479         FileInputStream fis = null;
480         JsonReader jr = null;
481         Log logger = this.getLog();
482 
483         logger.debug("Looking for the file '" + filePath + "'");
484         File file = new File(filePath);
485 
486         if (!(file.exists() && file.isFile())) {
487             logger.error("The file '" + filePath
488                     + "' either doesn't exist or not a file.");
489             throw new FileNotFoundException();
490         }
491 
492         fis = new FileInputStream(file);
493         jr = new JsonReader(fis,true);
494         JsonObject<String,Object> source = (JsonObject<String,Object>)jr.readObject();
495         Object[] objs = (Object[])source.get("@items");
496         
497         CucumberStepSource[] sources = new CucumberStepSource[objs.length];
498         for(int i=0;i<objs.length;i++){
499             sources[i] = new CucumberStepSource((JsonObject<String,Object>)objs[i]);
500         }
501         jr.close();
502         fis.close();
503         return sources;
504     }
505     
506     /*
507      * (non-Javadoc)
508      * 
509      * @see
510      * org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util
511      * .Locale)
512      */
513     @Override
514     protected void executeReport(Locale arg0) throws MavenReportException {
515         try {
516             
517             CucumberStepSource[] sources = getStepSources(jsonUsageFile);
518             
519             Sink sink = getSink();
520             sink.head();
521             sink.title();
522             sink.text("Cucumber Steps Usage Report");
523             sink.title_();
524             sink.head_();
525          
526             sink.body();
527             sink.section1();
528             sink.sectionTitle1();
529             sink.text("Cucumber Usage Statistics");
530             sink.sectionTitle1_();
531             
532             sink.section2();
533             sink.sectionTitle2();
534             sink.text("Overview Graph");
535             sink.sectionTitle2_();
536             sink.paragraph();
537             generateUsageOverviewGraphReport(sink,sources);
538             sink.paragraph_();
539             sink.section2_();
540             
541             sink.section2();
542             sink.sectionTitle2();
543             sink.text("Overview Table");
544             sink.sectionTitle2_();
545             sink.paragraph();
546             generateUsageOverviewTableReport(sink,sources);
547             sink.paragraph_();
548             sink.section2_();
549 
550             sink.section1();
551             sink.sectionTitle1();
552             sink.text("Cucumber Usage Detailed Information");
553             sink.sectionTitle1_();
554             sink.paragraph();
555             generateUsageDetailedReport(sink,sources);
556             sink.paragraph_();
557             sink.section1_();
558             sink.flush();
559             sink.close();
560             
561         } catch (Exception e) {
562             throw new MavenReportException(
563                     "Error occured while generating Cucumber usage report", e);
564         }
565     }
566 }