|
22 | 22 | import org.apache.maven.reporting.AbstractMavenReport; |
23 | 23 | import org.apache.maven.reporting.MavenReportException; |
24 | 24 | import org.hjug.cbc.CostBenefitCalculator; |
| 25 | +import org.hjug.cbc.RankedCycle; |
25 | 26 | import org.hjug.cbc.RankedDisharmony; |
26 | 27 | import org.hjug.gdg.GraphDataGenerator; |
27 | 28 | import org.hjug.git.GitLogReader; |
| 29 | +import org.jgrapht.Graph; |
| 30 | +import org.jgrapht.graph.DefaultWeightedEdge; |
28 | 31 |
|
29 | 32 | @Slf4j |
30 | 33 | @Mojo( |
@@ -65,6 +68,14 @@ public String getDescription(Locale locale) { |
65 | 68 | + " have the highest priority values."; |
66 | 69 | } |
67 | 70 |
|
| 71 | + public final String[] cycleTableHeadings = { |
| 72 | + "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Minimum Cuts" |
| 73 | + }; |
| 74 | + |
| 75 | + public final String[] classCycleTableHeadings = {"Classes", "Relationships"}; |
| 76 | + |
| 77 | + private Graph<String, DefaultWeightedEdge> classGraph; |
| 78 | + |
68 | 79 | @Override |
69 | 80 | public void executeReport(Locale locale) throws MavenReportException { |
70 | 81 |
|
@@ -127,9 +138,6 @@ public void executeReport(Locale locale) throws MavenReportException { |
127 | 138 | * @See https://maven.apache.org/doxia/developers/sink.html#How_to_inject_javascript_code_into_HTML |
128 | 139 | */ |
129 | 140 | SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet(); |
130 | | - // githubButtonJS.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); |
131 | | - // githubButtonJS.addAttribute("async", ""); |
132 | | - // githubButtonJS.addAttribute("defer", ""); |
133 | 141 | githubButtonJS.addAttribute(SinkEventAttributes.SRC, "https://buttons.github.io/buttons.js"); |
134 | 142 |
|
135 | 143 | String script = "script"; |
@@ -211,18 +219,21 @@ public void executeReport(Locale locale) throws MavenReportException { |
211 | 219 |
|
212 | 220 | List<RankedDisharmony> rankedGodClassDisharmonies; |
213 | 221 | List<RankedDisharmony> rankedCBODisharmonies; |
| 222 | + List<RankedCycle> rankedCycles; |
214 | 223 | try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir)) { |
215 | 224 | costBenefitCalculator.runPmdAnalysis(); |
216 | 225 | rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); |
217 | 226 | rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); |
| 227 | + rankedCycles = runCycleAnalysis(costBenefitCalculator, outputDirectory.getPath()); |
| 228 | + classGraph = costBenefitCalculator.getClassReferencesGraph(); |
218 | 229 | } catch (Exception e) { |
219 | 230 | log.error("Error running analysis."); |
220 | 231 | throw new RuntimeException(e); |
221 | 232 | } |
222 | 233 |
|
223 | | - if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty()) { |
| 234 | + if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty() && rankedCycles.isEmpty()) { |
224 | 235 | mainSink.text("Contratulations! " + projectName + " " + projectVersion |
225 | | - + " has no God classes or highly coupled classes!"); |
| 236 | + + " has no God classes, highly coupled classes, or cycles!"); |
226 | 237 | mainSink.section1_(); |
227 | 238 | renderGitHubButtons(mainSink); |
228 | 239 | mainSink.body_(); |
@@ -418,13 +429,176 @@ public void executeReport(Locale locale) throws MavenReportException { |
418 | 429 | mainSink.tableRows_(); |
419 | 430 | mainSink.table_(); |
420 | 431 |
|
| 432 | + if (!rankedCycles.isEmpty()) { |
| 433 | + mainSink.lineBreak(); |
| 434 | + mainSink.lineBreak(); |
| 435 | + mainSink.horizontalRule(); |
| 436 | + mainSink.lineBreak(); |
| 437 | + mainSink.lineBreak(); |
| 438 | + |
| 439 | + renderCycles(outputDirectory.getPath(), mainSink, rankedCycles, formatter); |
| 440 | + } |
| 441 | + |
421 | 442 | // Close |
422 | 443 | mainSink.section1_(); |
423 | 444 | mainSink.body_(); |
424 | 445 |
|
425 | 446 | log.info("Done! View the report at target/site/{}", filename); |
426 | 447 | } |
427 | 448 |
|
| 449 | + public List<RankedCycle> runCycleAnalysis(CostBenefitCalculator costBenefitCalculator, String outputDirectory) { |
| 450 | + return costBenefitCalculator.runCycleAnalysis(outputDirectory, true); |
| 451 | + } |
| 452 | + |
| 453 | + private void renderCycles( |
| 454 | + String outputDirectory, Sink mainSink, List<RankedCycle> rankedCycles, DateTimeFormatter formatter) { |
| 455 | + |
| 456 | + SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); |
| 457 | + alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); |
| 458 | + |
| 459 | + mainSink.division(alignCenter); |
| 460 | + mainSink.section1(); |
| 461 | + mainSink.sectionTitle1(); |
| 462 | + mainSink.text("Class Cycles"); |
| 463 | + mainSink.sectionTitle1_(); |
| 464 | + mainSink.section1_(); |
| 465 | + mainSink.division_(); |
| 466 | + |
| 467 | + mainSink.division(alignCenter); |
| 468 | + mainSink.section2(); |
| 469 | + mainSink.sectionTitle2(); |
| 470 | + mainSink.text("Class Cycles by the numbers: (Refactor starting with Priority 1)"); |
| 471 | + mainSink.sectionTitle2_(); |
| 472 | + mainSink.section2_(); |
| 473 | + mainSink.division_(); |
| 474 | + |
| 475 | + mainSink.table(); |
| 476 | + mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); |
| 477 | + |
| 478 | + // Content |
| 479 | + // header row |
| 480 | + |
| 481 | + mainSink.tableRow(); |
| 482 | + for (String heading : cycleTableHeadings) { |
| 483 | + drawTableHeaderCell(heading, mainSink); |
| 484 | + } |
| 485 | + mainSink.tableRow_(); |
| 486 | + |
| 487 | + for (RankedCycle rankedCycle : rankedCycles) { |
| 488 | + mainSink.tableRow(); |
| 489 | + |
| 490 | + StringBuilder edgesToCut = new StringBuilder(); |
| 491 | + for (DefaultWeightedEdge minCutEdge : rankedCycle.getMinCutEdges()) { |
| 492 | + edgesToCut.append(minCutEdge + ":" + (int) classGraph.getEdgeWeight(minCutEdge)); |
| 493 | + } |
| 494 | + |
| 495 | + // "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Min Cuts" |
| 496 | + String[] rankedCycleData = { |
| 497 | + rankedCycle.getCycleName(), |
| 498 | + rankedCycle.getPriority().toString(), |
| 499 | + rankedCycle.getChangePronenessRank().toString(), |
| 500 | + String.valueOf(rankedCycle.getCycleNodes().size()), |
| 501 | + String.valueOf(rankedCycle.getEdgeSet().size()), |
| 502 | + edgesToCut.toString() |
| 503 | + }; |
| 504 | + |
| 505 | + for (String rowData : rankedCycleData) { |
| 506 | + drawCycleTableCell(rowData, mainSink); |
| 507 | + } |
| 508 | + |
| 509 | + mainSink.tableRow_(); |
| 510 | + } |
| 511 | + mainSink.tableRows_(); |
| 512 | + |
| 513 | + mainSink.table_(); |
| 514 | + |
| 515 | + for (RankedCycle rankedCycle : rankedCycles) { |
| 516 | + renderCycleTable(outputDirectory, mainSink, rankedCycle, formatter); |
| 517 | + } |
| 518 | + } |
| 519 | + |
| 520 | + private void renderCycleTable( |
| 521 | + String outputDirectory, Sink mainSink, RankedCycle cycle, DateTimeFormatter formatter) { |
| 522 | + |
| 523 | + mainSink.lineBreak(); |
| 524 | + mainSink.lineBreak(); |
| 525 | + mainSink.lineBreak(); |
| 526 | + mainSink.lineBreak(); |
| 527 | + mainSink.lineBreak(); |
| 528 | + |
| 529 | + SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); |
| 530 | + alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); |
| 531 | + |
| 532 | + mainSink.division(alignCenter); |
| 533 | + mainSink.section2(); |
| 534 | + mainSink.sectionTitle2(); |
| 535 | + mainSink.text("Class Cycle : " + cycle.getCycleName()); |
| 536 | + mainSink.sectionTitle2_(); |
| 537 | + mainSink.section2_(); |
| 538 | + mainSink.division_(); |
| 539 | + |
| 540 | + renderCycleImage(cycle.getCycleName(), mainSink, outputDirectory); |
| 541 | + |
| 542 | + mainSink.division(alignCenter); |
| 543 | + mainSink.bold(); |
| 544 | + mainSink.text("\"*\" indicates relationship(s) to remove to decompose cycle"); |
| 545 | + mainSink.bold_(); |
| 546 | + mainSink.division_(); |
| 547 | + |
| 548 | + mainSink.table(); |
| 549 | + mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); |
| 550 | + |
| 551 | + // Content |
| 552 | + mainSink.tableRow(); |
| 553 | + for (String heading : classCycleTableHeadings) { |
| 554 | + drawTableHeaderCell(heading, mainSink); |
| 555 | + } |
| 556 | + mainSink.tableRow_(); |
| 557 | + |
| 558 | + for (String vertex : cycle.getVertexSet()) { |
| 559 | + mainSink.tableRow(); |
| 560 | + drawTableCell(vertex, mainSink); |
| 561 | + StringBuilder edges = new StringBuilder(); |
| 562 | + for (org.jgrapht.graph.DefaultWeightedEdge edge : cycle.getEdgeSet()) { |
| 563 | + if (edge.toString().startsWith("(" + vertex + " :")) { |
| 564 | + if (cycle.getMinCutEdges().contains(edge)) { |
| 565 | + edges.append(edge); |
| 566 | + edges.append(":") |
| 567 | + .append((int) classGraph.getEdgeWeight(edge)) |
| 568 | + .append("*"); |
| 569 | + } else { |
| 570 | + edges.append(edge); |
| 571 | + edges.append(":").append((int) classGraph.getEdgeWeight(edge)); |
| 572 | + } |
| 573 | + } |
| 574 | + } |
| 575 | + drawCycleTableCell(edges.toString(), mainSink); |
| 576 | + mainSink.tableRow_(); |
| 577 | + } |
| 578 | + |
| 579 | + mainSink.tableRows_(); |
| 580 | + mainSink.table_(); |
| 581 | + } |
| 582 | + |
| 583 | + public void renderCycleImage(String cycleName, Sink mainSink, String outputDirectory) { |
| 584 | + SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); |
| 585 | + alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); |
| 586 | + mainSink.division(alignCenter); |
| 587 | + |
| 588 | + SinkEventAttributeSet imageAttributes = new SinkEventAttributeSet(); |
| 589 | + imageAttributes.addAttribute(SinkEventAttributes.TYPE, "img"); |
| 590 | + imageAttributes.addAttribute(SinkEventAttributes.SRC, "./refactorFirst/cycles/graph" + cycleName + ".png"); |
| 591 | + imageAttributes.addAttribute(SinkEventAttributes.WIDTH, 1000); |
| 592 | + imageAttributes.addAttribute(SinkEventAttributes.HEIGHT, 1000); |
| 593 | + imageAttributes.addAttribute(SinkEventAttributes.ALT, "Cycle " + cycleName); |
| 594 | + |
| 595 | + mainSink.unknown("img", new Object[] {HtmlMarkup.TAG_TYPE_SIMPLE}, imageAttributes); |
| 596 | + |
| 597 | + mainSink.division_(); |
| 598 | + mainSink.lineBreak(); |
| 599 | + mainSink.lineBreak(); |
| 600 | + } |
| 601 | + |
428 | 602 | private void renderLegend(Sink mainSink, String legendHeading, String xAxis) { |
429 | 603 | SinkEventAttributeSet width = new SinkEventAttributeSet(); |
430 | 604 | width.addAttribute(SinkEventAttributes.STYLE, "width:350px"); |
@@ -479,6 +653,31 @@ void drawTableCell(Object cellText, Sink mainSink) { |
479 | 653 | mainSink.tableCell_(); |
480 | 654 | } |
481 | 655 |
|
| 656 | + void drawCycleTableCell(String cellText, Sink mainSink) { |
| 657 | + SinkEventAttributeSet align = new SinkEventAttributeSet(); |
| 658 | + align.addAttribute(SinkEventAttributes.ALIGN, "left"); |
| 659 | + |
| 660 | + mainSink.tableCell(align); |
| 661 | + |
| 662 | + for (String string : cellText.split("\\(")) { |
| 663 | + if (string.contains("*")) { |
| 664 | + mainSink.bold(); |
| 665 | + mainSink.text("(" + string); |
| 666 | + mainSink.bold_(); |
| 667 | + } else { |
| 668 | + if (string.contains(")")) { |
| 669 | + mainSink.text("(" + string); |
| 670 | + } else { |
| 671 | + mainSink.text(string); |
| 672 | + } |
| 673 | + } |
| 674 | + |
| 675 | + mainSink.lineBreak(); |
| 676 | + } |
| 677 | + |
| 678 | + mainSink.tableCell_(); |
| 679 | + } |
| 680 | + |
482 | 681 | /* |
483 | 682 | <a class="github-button" href="https://github.com/jimbethancourt/refactorfirst" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star jimbethancourt/refactorfirst on GitHub">Star</a> |
484 | 683 | <a class="github-button" href="https://github.com/jimbethancourt/refactorfirst/fork" data-icon="octicon-repo-forked" data-size="large" data-show-count="true" aria-label="Fork jimbethancourt/refactorfirst on GitHub">Fork</a> |
|
0 commit comments