diff --git a/change-proneness-ranker/src/main/java/org/hjug/git/ChangePronenessRanker.java b/change-proneness-ranker/src/main/java/org/hjug/git/ChangePronenessRanker.java index 298f30d7..2b57a2cd 100644 --- a/change-proneness-ranker/src/main/java/org/hjug/git/ChangePronenessRanker.java +++ b/change-proneness-ranker/src/main/java/org/hjug/git/ChangePronenessRanker.java @@ -4,7 +4,6 @@ import java.util.*; import lombok.extern.slf4j.Slf4j; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.Repository; @Slf4j public class ChangePronenessRanker { @@ -12,10 +11,10 @@ public class ChangePronenessRanker { private final TreeMap changeCountsByTimeStamps = new TreeMap<>(); private final Map cachedScmLogInfos = new HashMap<>(); - public ChangePronenessRanker(Repository repository, GitLogReader repositoryLogReader) { + public ChangePronenessRanker(GitLogReader repositoryLogReader) { try { log.info("Capturing change count based on commit timestamps"); - changeCountsByTimeStamps.putAll(repositoryLogReader.captureChangeCountByCommitTimestamp(repository)); + changeCountsByTimeStamps.putAll(repositoryLogReader.captureChangeCountByCommitTimestamp()); } catch (IOException | GitAPIException e) { log.error("Error reading from repository: {}", e.getMessage()); } diff --git a/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java b/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java index c684850c..0add55ae 100644 --- a/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java +++ b/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java @@ -15,10 +15,39 @@ import org.eclipse.jgit.util.io.NullOutputStream; @Slf4j -public class GitLogReader { +public class GitLogReader implements AutoCloseable { static final String JAVA_FILE_TYPE = ".java"; + private Repository gitRepository; + + private Git git; + + public GitLogReader() {} + + public GitLogReader(File basedir) throws IOException { + FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder().findGitDir(basedir); + String gitIndexFileEnvVariable = System.getenv("GIT_INDEX_FILE"); + if (Objects.nonNull(gitIndexFileEnvVariable) + && !gitIndexFileEnvVariable.trim().isEmpty()) { + log.debug("Setting Index File based on Env Variable GIT_INDEX_FILE {}", gitIndexFileEnvVariable); + repositoryBuilder = repositoryBuilder.setIndexFile(new File(gitIndexFileEnvVariable)); + } + + git = Git.open(repositoryBuilder.getGitDir()); + gitRepository = git.getRepository(); + } + + GitLogReader(Git git) { + this.git = git; + gitRepository = git.getRepository(); + } + + @Override + public void close() throws Exception { + git.close(); + } + // Based on // https://github.com/Cosium/git-code-format-maven-plugin/blob/master/src/main/java/com/cosium/code/format/AbstractMavenGitCodeFormatMojo.java // MIT License @@ -81,14 +110,12 @@ public Map listRepositoryContentsAtHEAD(Repositor * Returns the number of commits and earliest commit for a given path * TODO: Move to a different class??? * - * @param repository * @param path * @return a LogInfo object * @throws GitAPIException */ - public ScmLogInfo fileLog(Repository repository, String path) throws GitAPIException, IOException { - Git git = new Git(repository); - ObjectId branchId = repository.resolve("HEAD"); + public ScmLogInfo fileLog(String path) throws GitAPIException, IOException { + ObjectId branchId = gitRepository.resolve("HEAD"); Iterable revCommits = git.log().add(branchId).addPath(path).call(); int commitCount = 0; @@ -101,8 +128,7 @@ public ScmLogInfo fileLog(Repository repository, String path) throws GitAPIExcep } // based on https://stackoverflow.com/a/59274329/346247 - int mostRecentCommit = new Git(repository) - .log() + int mostRecentCommit = git.log() .add(branchId) .addPath(path) .setMaxCount(1) @@ -115,49 +141,47 @@ public ScmLogInfo fileLog(Repository repository, String path) throws GitAPIExcep } // based on https://stackoverflow.com/questions/27361538/how-to-show-changes-between-commits-with-jgit - public TreeMap captureChangeCountByCommitTimestamp(Repository repository) - throws IOException, GitAPIException { + public TreeMap captureChangeCountByCommitTimestamp() throws IOException, GitAPIException { TreeMap changesByCommitTimestamp = new TreeMap<>(); - try (Git git = new Git(repository)) { - ObjectId branchId = repository.resolve("HEAD"); - Iterable commits = git.log().add(branchId).call(); + ObjectId branchId = gitRepository.resolve("HEAD"); + Iterable commits = git.log().add(branchId).call(); - RevCommit newCommit = null; + RevCommit newCommit = null; - for (Iterator iterator = commits.iterator(); iterator.hasNext(); ) { - RevCommit oldCommit = iterator.next(); - - int count = 0; - if (null == newCommit) { - newCommit = oldCommit; - continue; - } + for (Iterator iterator = commits.iterator(); iterator.hasNext(); ) { + RevCommit oldCommit = iterator.next(); - for (DiffEntry entry : getDiffEntries(git, newCommit, oldCommit)) { - if (entry.getNewPath().endsWith(JAVA_FILE_TYPE) - || entry.getOldPath().endsWith(JAVA_FILE_TYPE)) { - count++; - } - } + int count = 0; + if (null == newCommit) { + newCommit = oldCommit; + continue; + } - if (count > 0) { - changesByCommitTimestamp.put(newCommit.getCommitTime(), count); + for (DiffEntry entry : getDiffEntries(newCommit, oldCommit)) { + if (entry.getNewPath().endsWith(JAVA_FILE_TYPE) + || entry.getOldPath().endsWith(JAVA_FILE_TYPE)) { + count++; } + } - // Handle first / initial commit - if (!iterator.hasNext()) { - changesByCommitTimestamp.putAll(walkFirstCommit(repository, oldCommit)); - } + if (count > 0) { + changesByCommitTimestamp.put(newCommit.getCommitTime(), count); + } - newCommit = oldCommit; + // Handle first / initial commit + if (!iterator.hasNext()) { + changesByCommitTimestamp.putAll(walkFirstCommit(oldCommit)); } + + newCommit = oldCommit; } + return changesByCommitTimestamp; } - private List getDiffEntries(Git git, RevCommit newCommit, RevCommit oldCommit) throws IOException { + private List getDiffEntries(RevCommit newCommit, RevCommit oldCommit) throws IOException { CanonicalTreeParser oldTreeIter = new CanonicalTreeParser(); CanonicalTreeParser newTreeIter = new CanonicalTreeParser(); try (ObjectReader reader = git.getRepository().newObjectReader()) { @@ -172,11 +196,11 @@ private List getDiffEntries(Git git, RevCommit newCommit, RevCommit o return df.scan(oldTreeIter, newTreeIter); } - Map walkFirstCommit(Repository repository, RevCommit firstCommit) throws IOException { + Map walkFirstCommit(RevCommit firstCommit) throws IOException { Map changesByCommitTimestamp = new TreeMap<>(); int firstCommitCount = 0; ObjectId treeId = firstCommit.getTree(); - try (TreeWalk treeWalk = new TreeWalk(repository)) { + try (TreeWalk treeWalk = new TreeWalk(gitRepository)) { treeWalk.setRecursive(false); treeWalk.reset(treeId); while (treeWalk.next()) { diff --git a/change-proneness-ranker/src/test/java/org/hjug/git/ChangePronenessRankerTest.java b/change-proneness-ranker/src/test/java/org/hjug/git/ChangePronenessRankerTest.java index daad2c61..7f5f883c 100644 --- a/change-proneness-ranker/src/test/java/org/hjug/git/ChangePronenessRankerTest.java +++ b/change-proneness-ranker/src/test/java/org/hjug/git/ChangePronenessRankerTest.java @@ -1,6 +1,5 @@ package org.hjug.git; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -31,9 +30,9 @@ void testChangePronenessCalculation() throws IOException, GitAPIException { commitsWithChangeCounts.put(scmLogInfo.getEarliestCommit() + 5 * 60, 3); commitsWithChangeCounts.put(scmLogInfo.getEarliestCommit() + 10 * 60, 3); - when(repositoryLogReader.captureChangeCountByCommitTimestamp(any())).thenReturn(commitsWithChangeCounts); + when(repositoryLogReader.captureChangeCountByCommitTimestamp()).thenReturn(commitsWithChangeCounts); - changePronenessRanker = new ChangePronenessRanker(null, repositoryLogReader); + changePronenessRanker = new ChangePronenessRanker(repositoryLogReader); List scmLogInfos = new ArrayList<>(); scmLogInfos.add(scmLogInfo); changePronenessRanker.rankChangeProneness(scmLogInfos); @@ -57,8 +56,8 @@ void testRankChangeProneness() throws IOException, GitAPIException { commitsWithChangeCounts.put(scmLogInfo2.getEarliestCommit() + 5 * 60, 5); commitsWithChangeCounts.put(scmLogInfo2.getEarliestCommit() + 10 * 60, 5); - when(repositoryLogReader.captureChangeCountByCommitTimestamp(any())).thenReturn(commitsWithChangeCounts); - changePronenessRanker = new ChangePronenessRanker(null, repositoryLogReader); + when(repositoryLogReader.captureChangeCountByCommitTimestamp()).thenReturn(commitsWithChangeCounts); + changePronenessRanker = new ChangePronenessRanker(repositoryLogReader); List scmLogInfos = new ArrayList<>(); scmLogInfos.add(scmLogInfo); diff --git a/change-proneness-ranker/src/test/java/org/hjug/git/GitLogReaderTest.java b/change-proneness-ranker/src/test/java/org/hjug/git/GitLogReaderTest.java index b436949d..6a05ed8d 100644 --- a/change-proneness-ranker/src/test/java/org/hjug/git/GitLogReaderTest.java +++ b/change-proneness-ranker/src/test/java/org/hjug/git/GitLogReaderTest.java @@ -40,7 +40,7 @@ void testFileLog() throws IOException, GitAPIException, InterruptedException { // This path works when referencing the full Tobago repository // String filePath = "tobago-core/src/main/java/org/apache/myfaces/tobago/facelets/AttributeHandler.java"; - GitLogReader gitLogReader = new GitLogReader(); + GitLogReader gitLogReader = new GitLogReader(git); String attributeHandler = "AttributeHandler.java"; InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler); @@ -59,7 +59,7 @@ void testFileLog() throws IOException, GitAPIException, InterruptedException { git.add().addFilepattern(".").call(); RevCommit secondCommit = git.commit().setMessage("message").call(); - ScmLogInfo scmLogInfo = gitLogReader.fileLog(repository, attributeHandler); + ScmLogInfo scmLogInfo = gitLogReader.fileLog(attributeHandler); Assertions.assertEquals(2, scmLogInfo.getCommitCount()); Assertions.assertEquals(firstCommit.getCommitTime(), scmLogInfo.getEarliestCommit()); @@ -68,7 +68,7 @@ void testFileLog() throws IOException, GitAPIException, InterruptedException { @Test void testWalkFirstCommit() throws IOException, GitAPIException { - GitLogReader gitLogReader = new GitLogReader(); + GitLogReader gitLogReader = new GitLogReader(git); String attributeHandler = "AttributeHandler.java"; InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler); @@ -76,7 +76,7 @@ void testWalkFirstCommit() throws IOException, GitAPIException { git.add().addFilepattern(".").call(); RevCommit commit = git.commit().setMessage("message").call(); - Map result = gitLogReader.walkFirstCommit(repository, commit); + Map result = gitLogReader.walkFirstCommit(commit); Assertions.assertTrue(result.containsKey(commit.getCommitTime())); Assertions.assertEquals(1, result.get(commit.getCommitTime()).intValue()); @@ -84,7 +84,7 @@ void testWalkFirstCommit() throws IOException, GitAPIException { @Test void testCaptureChangCountByCommitTimestamp() throws Exception { - GitLogReader gitLogReader = new GitLogReader(); + GitLogReader gitLogReader = new GitLogReader(git); String attributeHandler = "AttributeHandler.java"; InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler); @@ -106,7 +106,7 @@ void testCaptureChangCountByCommitTimestamp() throws Exception { git.add().addFilepattern(".").call(); RevCommit secondCommit = git.commit().setMessage("message").call(); - Map commitCounts = gitLogReader.captureChangeCountByCommitTimestamp(repository); + Map commitCounts = gitLogReader.captureChangeCountByCommitTimestamp(); Assertions.assertEquals(1, commitCounts.get(firstCommit.getCommitTime()).intValue()); Assertions.assertEquals( diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java index a2bc7ff3..d8e91fb3 100644 --- a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java @@ -18,7 +18,6 @@ import net.sourceforge.pmd.*; import net.sourceforge.pmd.lang.LanguageRegistry; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.Repository; import org.hjug.cycledetector.CircularReferenceChecker; import org.hjug.git.ChangePronenessRanker; import org.hjug.git.GitLogReader; @@ -33,14 +32,14 @@ import org.jgrapht.graph.DefaultWeightedEdge; @Slf4j -public class CostBenefitCalculator { +public class CostBenefitCalculator implements AutoCloseable { private final Map> renderedSubGraphs = new HashMap<>(); private Report report; private String repositoryPath; - private final GitLogReader gitLogReader = new GitLogReader(); - private Repository repository = null; + private GitLogReader gitLogReader; + private final ChangePronenessRanker changePronenessRanker; private final JavaProjectParser javaProjectParser = new JavaProjectParser(); @@ -52,16 +51,22 @@ public CostBenefitCalculator(String repositoryPath) { log.info("Initiating Cost Benefit calculation"); try { - repository = gitLogReader.gitRepository(new File(repositoryPath)); - for (String file : - gitLogReader.listRepositoryContentsAtHEAD(repository).keySet()) { - log.info("Files at HEAD: {}", file); - } + gitLogReader = new GitLogReader(new File(repositoryPath)); + // repository = gitLogReader.gitRepository(new File(repositoryPath)); + // for (String file : + // gitLogReader.listRepositoryContentsAtHEAD(repository).keySet()) { + // log.info("Files at HEAD: {}", file); + // } } catch (IOException e) { log.error("Failure to access Git repository", e); } - changePronenessRanker = new ChangePronenessRanker(repository, gitLogReader); + changePronenessRanker = new ChangePronenessRanker(gitLogReader); + } + + @Override + public void close() throws Exception { + gitLogReader.close(); } public List runCycleAnalysis(String outputDirectoryPath, boolean renderImages) { @@ -148,7 +153,7 @@ public List runCycleAnalysis(String outputDirectoryPath, boolean re private boolean isDuplicateSubGraph(AsSubgraph subGraph, String vertex) { if (!renderedSubGraphs.isEmpty()) { - for (AsSubgraph renderedSubGraph : renderedSubGraphs.values()) { + for (AsSubgraph renderedSubGraph : renderedSubGraphs.values()) { if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size() && renderedSubGraph.edgeSet().size() == subGraph.edgeSet().size() @@ -176,7 +181,7 @@ public void runPmdAnalysis() throws IOException { log.info("files to be scanned: " + Paths.get(repositoryPath)); try (Stream files = Files.walk(Paths.get(repositoryPath))) { - files.forEach(file -> pmd.files().addFile(file)); + files.filter(Files::isRegularFile).forEach(file -> pmd.files().addFile(file)); } report = pmd.performAnalysisAndCollectReport(); @@ -231,25 +236,23 @@ private List getGodClasses() { } List getRankedChangeProneness(List disharmonies) { - List scmLogInfos = new ArrayList<>(); log.info("Calculating Change Proneness"); - for (Disharmony disharmony : disharmonies) { - String path = disharmony.getFileName(); - ScmLogInfo scmLogInfo = null; - try { - scmLogInfo = gitLogReader.fileLog(repository, path); - log.info("Successfully fetched scmLogInfo for {}", scmLogInfo.getPath()); - } catch (GitAPIException | IOException e) { - log.error("Error reading Git repository contents.", e); - } catch (NullPointerException e) { - log.error("Encountered nested class in a class containing a violation. Class: {}", path); - } - if (null != scmLogInfo) { - log.info("adding {}", scmLogInfo.getPath()); - scmLogInfos.add(scmLogInfo); - } - } + List scmLogInfos = disharmonies.parallelStream() + .map(disharmony -> { + String path = disharmony.getFileName(); + ScmLogInfo scmLogInfo = null; + try { + scmLogInfo = gitLogReader.fileLog(path); + log.info("Successfully fetched scmLogInfo for {}", scmLogInfo.getPath()); + } catch (GitAPIException | IOException e) { + log.error("Error reading Git repository contents.", e); + } catch (NullPointerException e) { + log.error("Encountered nested class in a class containing a violation. Class: {}", path); + } + return scmLogInfo; + }) + .collect(Collectors.toList()); changePronenessRanker.rankChangeProneness(scmLogInfos); return scmLogInfos; diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java index 60f596d8..cdeaccc5 100644 --- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java @@ -22,9 +22,12 @@ import org.apache.maven.reporting.AbstractMavenReport; import org.apache.maven.reporting.MavenReportException; import org.hjug.cbc.CostBenefitCalculator; +import org.hjug.cbc.RankedCycle; import org.hjug.cbc.RankedDisharmony; import org.hjug.gdg.GraphDataGenerator; import org.hjug.git.GitLogReader; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; @Slf4j @Mojo( @@ -65,6 +68,14 @@ public String getDescription(Locale locale) { + " have the highest priority values."; } + public final String[] cycleTableHeadings = { + "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Minimum Cuts" + }; + + public final String[] classCycleTableHeadings = {"Classes", "Relationships"}; + + private Graph classGraph; + @Override public void executeReport(Locale locale) throws MavenReportException { @@ -127,9 +138,6 @@ public void executeReport(Locale locale) throws MavenReportException { * @See https://maven.apache.org/doxia/developers/sink.html#How_to_inject_javascript_code_into_HTML */ SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet(); - // githubButtonJS.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - // githubButtonJS.addAttribute("async", ""); - // githubButtonJS.addAttribute("defer", ""); githubButtonJS.addAttribute(SinkEventAttributes.SRC, "https://buttons.github.io/buttons.js"); String script = "script"; @@ -209,20 +217,23 @@ public void executeReport(Locale locale) throws MavenReportException { return; } - CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir); - try { + List rankedGodClassDisharmonies; + List rankedCBODisharmonies; + List rankedCycles; + try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir)) { costBenefitCalculator.runPmdAnalysis(); - } catch (IOException e) { - log.error("Error running PMD analysis."); + rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); + rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); + rankedCycles = runCycleAnalysis(costBenefitCalculator, outputDirectory.getPath()); + classGraph = costBenefitCalculator.getClassReferencesGraph(); + } catch (Exception e) { + log.error("Error running analysis."); throw new RuntimeException(e); } - List rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); - - List rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); - if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty()) { + if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty() && rankedCycles.isEmpty()) { mainSink.text("Contratulations! " + projectName + " " + projectVersion - + " has no God classes or highly coupled classes!"); + + " has no God classes, highly coupled classes, or cycles!"); mainSink.section1_(); renderGitHubButtons(mainSink); mainSink.body_(); @@ -418,6 +429,16 @@ public void executeReport(Locale locale) throws MavenReportException { mainSink.tableRows_(); mainSink.table_(); + if (!rankedCycles.isEmpty()) { + mainSink.lineBreak(); + mainSink.lineBreak(); + mainSink.horizontalRule(); + mainSink.lineBreak(); + mainSink.lineBreak(); + + renderCycles(outputDirectory.getPath(), mainSink, rankedCycles, formatter); + } + // Close mainSink.section1_(); mainSink.body_(); @@ -425,6 +446,159 @@ public void executeReport(Locale locale) throws MavenReportException { log.info("Done! View the report at target/site/{}", filename); } + public List runCycleAnalysis(CostBenefitCalculator costBenefitCalculator, String outputDirectory) { + return costBenefitCalculator.runCycleAnalysis(outputDirectory, true); + } + + private void renderCycles( + String outputDirectory, Sink mainSink, List rankedCycles, DateTimeFormatter formatter) { + + SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); + alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); + + mainSink.division(alignCenter); + mainSink.section1(); + mainSink.sectionTitle1(); + mainSink.text("Class Cycles"); + mainSink.sectionTitle1_(); + mainSink.section1_(); + mainSink.division_(); + + mainSink.division(alignCenter); + mainSink.section2(); + mainSink.sectionTitle2(); + mainSink.text("Class Cycles by the numbers: (Refactor starting with Priority 1)"); + mainSink.sectionTitle2_(); + mainSink.section2_(); + mainSink.division_(); + + mainSink.table(); + mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); + + // Content + // header row + + mainSink.tableRow(); + for (String heading : cycleTableHeadings) { + drawTableHeaderCell(heading, mainSink); + } + mainSink.tableRow_(); + + for (RankedCycle rankedCycle : rankedCycles) { + mainSink.tableRow(); + + StringBuilder edgesToCut = new StringBuilder(); + for (DefaultWeightedEdge minCutEdge : rankedCycle.getMinCutEdges()) { + edgesToCut.append(minCutEdge + ":" + (int) classGraph.getEdgeWeight(minCutEdge)); + } + + // "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Min Cuts" + String[] rankedCycleData = { + rankedCycle.getCycleName(), + rankedCycle.getPriority().toString(), + rankedCycle.getChangePronenessRank().toString(), + String.valueOf(rankedCycle.getCycleNodes().size()), + String.valueOf(rankedCycle.getEdgeSet().size()), + edgesToCut.toString() + }; + + for (String rowData : rankedCycleData) { + drawCycleTableCell(rowData, mainSink); + } + + mainSink.tableRow_(); + } + mainSink.tableRows_(); + + mainSink.table_(); + + for (RankedCycle rankedCycle : rankedCycles) { + renderCycleTable(outputDirectory, mainSink, rankedCycle, formatter); + } + } + + private void renderCycleTable( + String outputDirectory, Sink mainSink, RankedCycle cycle, DateTimeFormatter formatter) { + + mainSink.lineBreak(); + mainSink.lineBreak(); + mainSink.lineBreak(); + mainSink.lineBreak(); + mainSink.lineBreak(); + + SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); + alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); + + mainSink.division(alignCenter); + mainSink.section2(); + mainSink.sectionTitle2(); + mainSink.text("Class Cycle : " + cycle.getCycleName()); + mainSink.sectionTitle2_(); + mainSink.section2_(); + mainSink.division_(); + + renderCycleImage(cycle.getCycleName(), mainSink, outputDirectory); + + mainSink.division(alignCenter); + mainSink.bold(); + mainSink.text("\"*\" indicates relationship(s) to remove to decompose cycle"); + mainSink.bold_(); + mainSink.division_(); + + mainSink.table(); + mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); + + // Content + mainSink.tableRow(); + for (String heading : classCycleTableHeadings) { + drawTableHeaderCell(heading, mainSink); + } + mainSink.tableRow_(); + + for (String vertex : cycle.getVertexSet()) { + mainSink.tableRow(); + drawTableCell(vertex, mainSink); + StringBuilder edges = new StringBuilder(); + for (org.jgrapht.graph.DefaultWeightedEdge edge : cycle.getEdgeSet()) { + if (edge.toString().startsWith("(" + vertex + " :")) { + if (cycle.getMinCutEdges().contains(edge)) { + edges.append(edge); + edges.append(":") + .append((int) classGraph.getEdgeWeight(edge)) + .append("*"); + } else { + edges.append(edge); + edges.append(":").append((int) classGraph.getEdgeWeight(edge)); + } + } + } + drawCycleTableCell(edges.toString(), mainSink); + mainSink.tableRow_(); + } + + mainSink.tableRows_(); + mainSink.table_(); + } + + public void renderCycleImage(String cycleName, Sink mainSink, String outputDirectory) { + SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); + alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); + mainSink.division(alignCenter); + + SinkEventAttributeSet imageAttributes = new SinkEventAttributeSet(); + imageAttributes.addAttribute(SinkEventAttributes.TYPE, "img"); + imageAttributes.addAttribute(SinkEventAttributes.SRC, "./refactorFirst/cycles/graph" + cycleName + ".png"); + imageAttributes.addAttribute(SinkEventAttributes.WIDTH, 1000); + imageAttributes.addAttribute(SinkEventAttributes.HEIGHT, 1000); + imageAttributes.addAttribute(SinkEventAttributes.ALT, "Cycle " + cycleName); + + mainSink.unknown("img", new Object[] {HtmlMarkup.TAG_TYPE_SIMPLE}, imageAttributes); + + mainSink.division_(); + mainSink.lineBreak(); + mainSink.lineBreak(); + } + private void renderLegend(Sink mainSink, String legendHeading, String xAxis) { SinkEventAttributeSet width = new SinkEventAttributeSet(); width.addAttribute(SinkEventAttributes.STYLE, "width:350px"); @@ -479,6 +653,31 @@ void drawTableCell(Object cellText, Sink mainSink) { mainSink.tableCell_(); } + void drawCycleTableCell(String cellText, Sink mainSink) { + SinkEventAttributeSet align = new SinkEventAttributeSet(); + align.addAttribute(SinkEventAttributes.ALIGN, "left"); + + mainSink.tableCell(align); + + for (String string : cellText.split("\\(")) { + if (string.contains("*")) { + mainSink.bold(); + mainSink.text("(" + string); + mainSink.bold_(); + } else { + if (string.contains(")")) { + mainSink.text("(" + string); + } else { + mainSink.text(string); + } + } + + mainSink.lineBreak(); + } + + mainSink.tableCell_(); + } + /* Star Fork diff --git a/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java b/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java index 29320725..30ee3455 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java @@ -3,7 +3,6 @@ import static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk; import java.io.File; -import java.io.IOException; import java.nio.file.Paths; import java.time.Instant; import java.time.ZoneId; @@ -83,14 +82,14 @@ public void execute( } // actual calcualte - CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir); - try { + List rankedDisharmonies; + try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir)) { costBenefitCalculator.runPmdAnalysis(); - } catch (IOException e) { - log.error("Error running PMD analysis."); + rankedDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); + } catch (Exception e) { + log.error("Error running analysis."); throw new RuntimeException(e); } - List rankedDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); rankedDisharmonies.sort(Comparator.comparing(RankedDisharmony::getPriority)); diff --git a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java index 74a69244..d4afae0b 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java @@ -3,7 +3,6 @@ import static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk; import java.io.File; -import java.io.IOException; import java.nio.file.Paths; import java.time.Instant; import java.time.ZoneId; @@ -84,7 +83,6 @@ public class SimpleHtmlReport { "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Minimum Cuts" }; - // public final String[] classCycleTableHeadings = {"Classes", "Relationships", "Min Cut Edges"}; public final String[] classCycleTableHeadings = {"Classes", "Relationships"}; private Graph classGraph; @@ -154,18 +152,19 @@ public void execute( return; } - CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir); - try { + List rankedGodClassDisharmonies; + List rankedCBODisharmonies; + List rankedCycles; + try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir)) { costBenefitCalculator.runPmdAnalysis(); - } catch (IOException e) { - log.error("Error running PMD analysis."); + rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); + rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); + rankedCycles = runCycleAnalysis(costBenefitCalculator, outputDirectory); + classGraph = costBenefitCalculator.getClassReferencesGraph(); + } catch (Exception e) { + log.error("Error running analysis."); throw new RuntimeException(e); } - List rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); - List rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); - - List rankedCycles = runCycleAnalysis(costBenefitCalculator, outputDirectory); - classGraph = costBenefitCalculator.getClassReferencesGraph(); if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty() && rankedCycles.isEmpty()) { stringBuilder @@ -255,6 +254,7 @@ private void renderCycles( for (String heading : cycleTableHeadings) { stringBuilder.append("").append(heading).append(""); } + stringBuilder.append(""); stringBuilder.append(""); for (RankedCycle rankedCycle : rankedCycles) { @@ -284,9 +284,6 @@ private void renderCycles( } stringBuilder.append(""); - - stringBuilder.append(""); - stringBuilder.append(""); for (RankedCycle rankedCycle : rankedCycles) { @@ -319,6 +316,7 @@ private void renderCycleTable( for (String heading : classCycleTableHeadings) { stringBuilder.append("").append(heading).append(""); } + stringBuilder.append(""); stringBuilder.append(""); @@ -349,8 +347,6 @@ private void renderCycleTable( stringBuilder.append(""); - stringBuilder.append(""); - stringBuilder.append(""); }