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
39
40
41
42
43
44 public class CucumberUsageReportingPlugin extends AbstractMavenReport {
45
46
47
48
49
50
51 private String jsonUsageFile;
52
53
54
55
56
57
58
59 private String outputDirectory;
60
61
62
63
64
65
66 private MavenProject project;
67
68
69
70
71
72
73 private Renderer siteRenderer;
74
75
76
77
78
79
80
81 @Override
82 public String getDescription(Locale arg0) {
83 return "HTML formatted Cucumber keywords usage report";
84 }
85
86
87
88
89
90
91 @Override
92 public String getName(Locale arg0) {
93 return "Cucumber usage report";
94 }
95
96
97
98
99
100
101 @Override
102 public String getOutputName() {
103 return "cucumber-usage-report";
104 }
105
106
107
108
109
110
111 @Override
112 protected String getOutputDirectory() {
113 return this.outputDirectory;
114 }
115
116
117
118
119
120
121 @Override
122 protected MavenProject getProject() {
123 return this.project;
124 }
125
126
127
128
129
130
131 @Override
132 protected Renderer getSiteRenderer() {
133 return this.siteRenderer;
134 }
135
136
137
138
139 public String getJsonUsageFile() {
140 return jsonUsageFile;
141 }
142
143
144
145
146
147 public void setJsonUsageFile(String jsonUsageFile) {
148 this.jsonUsageFile = jsonUsageFile;
149 }
150
151
152
153
154
155 public void setOutputDirectory(String outputDirectory) {
156 this.outputDirectory = outputDirectory;
157 }
158
159
160
161
162
163 public void setProject(MavenProject project) {
164 this.project = project;
165 }
166
167
168
169
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;
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
276
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
376
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
403
404
405
406
407
408
409
410
411
412
413
414
415
416 sink.table_();
417 }
418
419
420
421
422
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
452 sink.text("-");
453 sink.tableCell_();
454 sink.tableCell();
455
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
508
509
510
511
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 }