diff --git a/circular-reference-detector/LICENSE b/circular-reference-detector/LICENSE new file mode 100644 index 00000000..0df1eda7 --- /dev/null +++ b/circular-reference-detector/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Nikhil Pereira + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/circular-reference-detector/README.md b/circular-reference-detector/README.md new file mode 100644 index 00000000..a9fbfba4 --- /dev/null +++ b/circular-reference-detector/README.md @@ -0,0 +1,10 @@ +# Java Circular Reference Detector + +Tool to create graph images of cyclic references in a java project. + +Usage : +- Pass two command line arguments: + - File path of source directory of the java project. + - Output directory path to store images of the circular reference graphs. + +By [Ideacrest Solutions](https://www.ideacrestsolutions.com/) diff --git a/circular-reference-detector/pom.xml b/circular-reference-detector/pom.xml new file mode 100644 index 00000000..62a29003 --- /dev/null +++ b/circular-reference-detector/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + + + org.hjug.refactorfirst + refactor-first + 0.5.0-M2-SNAPSHOT + + + org.hjug.refactorfirst.circularreferencedetector + circular-reference-detector + + Tool to help detecting circular references by parsing a java project. + + + + org.jgrapht + jgrapht-core + 1.3.0 + + + org.jgrapht + jgrapht-ext + 1.3.0 + + + com.github.javaparser + javaparser-symbol-solver-core + 3.26.1 + + + org.junit.jupiter + junit-jupiter-api + 5.5.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.5.2 + test + + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + + \ No newline at end of file diff --git a/circular-reference-detector/src/main/java/org/hjug/app/CircularReferenceDetectorApp.java b/circular-reference-detector/src/main/java/org/hjug/app/CircularReferenceDetectorApp.java new file mode 100644 index 00000000..8f34c6ab --- /dev/null +++ b/circular-reference-detector/src/main/java/org/hjug/app/CircularReferenceDetectorApp.java @@ -0,0 +1,107 @@ +package org.hjug.app; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.hjug.cycledetector.CircularReferenceChecker; +import org.hjug.parser.JavaProjectParser; +import org.jgrapht.Graph; +import org.jgrapht.alg.flow.GusfieldGomoryHuCutTree; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.AsUndirectedGraph; +import org.jgrapht.graph.DefaultEdge; + +/** + * Command line application to detect circular references in a java project. + * Takes two arguments : source folder of java project, directory to store images of the circular reference graphs. + * + * @author nikhil_pereira + */ +public class CircularReferenceDetectorApp { + + private Map renderedSubGraphs = new HashMap<>(); + + public static void main(String[] args) { + CircularReferenceDetectorApp circularReferenceDetectorApp = new CircularReferenceDetectorApp(); + circularReferenceDetectorApp.launchApp(args); + } + + /** + * Parses source project files and creates a graph of class references of the java project. + * Detects cycles in the class references graph and stores the cycle graphs in the given output directory + * + * @param args + */ + public void launchApp(String[] args) { + if (!validateArgs(args)) { + printCommandUsage(); + } else { + String srcDirectoryPath = args[0]; + String outputDirectoryPath = args[1]; + JavaProjectParser javaProjectParser = new JavaProjectParser(); + try { + Graph classReferencesGraph = + javaProjectParser.getClassReferences(srcDirectoryPath); + detectAndStoreCyclesInDirectory(outputDirectoryPath, classReferencesGraph); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void detectAndStoreCyclesInDirectory( + String outputDirectoryPath, Graph classReferencesGraph) { + CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker(); + Map> cyclesForEveryVertexMap = + circularReferenceChecker.detectCycles(classReferencesGraph); + cyclesForEveryVertexMap.forEach((vertex, subGraph) -> { + try { + int vertexCount = subGraph.vertexSet().size(); + int edgeCount = subGraph.edgeSet().size(); + if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) { + circularReferenceChecker.createImage(outputDirectoryPath, subGraph, vertex); + renderedSubGraphs.put(vertex, subGraph); + System.out.println( + "Vertex: " + vertex + " vertex count: " + vertexCount + " edge count: " + edgeCount); + GusfieldGomoryHuCutTree gusfieldGomoryHuCutTree = + new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph)); + double minCut = gusfieldGomoryHuCutTree.calculateMinCut(); + System.out.println("Min cut weight: " + minCut); + Set minCutEdges = gusfieldGomoryHuCutTree.getCutEdges(); + System.out.println("Minimum Cut Edges:"); + for (DefaultEdge minCutEdge : minCutEdges) { + System.out.println(minCutEdge); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private boolean isDuplicateSubGraph(AsSubgraph subGraph, String vertex) { + if (!renderedSubGraphs.isEmpty()) { + for (AsSubgraph renderedSubGraph : renderedSubGraphs.values()) { + if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size() + && renderedSubGraph.edgeSet().size() + == subGraph.edgeSet().size() + && renderedSubGraph.vertexSet().contains(vertex)) { + return true; + } + } + } + + return false; + } + + private boolean validateArgs(String[] args) { + return args.length == 2; + } + + private void printCommandUsage() { + System.out.println("Usage:\n" + + "argument 1 : file path of source directory of the java project." + + "argument 2 : output directory path to store images of the circular reference graphs."); + } +} diff --git a/circular-reference-detector/src/main/java/org/hjug/cycledetector/CircularReferenceChecker.java b/circular-reference-detector/src/main/java/org/hjug/cycledetector/CircularReferenceChecker.java new file mode 100644 index 00000000..2cbe18ed --- /dev/null +++ b/circular-reference-detector/src/main/java/org/hjug/cycledetector/CircularReferenceChecker.java @@ -0,0 +1,63 @@ +package org.hjug.cycledetector; + +import com.mxgraph.layout.mxCircleLayout; +import com.mxgraph.layout.mxIGraphLayout; +import com.mxgraph.util.mxCellRenderer; +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.imageio.ImageIO; +import org.jgrapht.Graph; +import org.jgrapht.alg.cycle.CycleDetector; +import org.jgrapht.ext.JGraphXAdapter; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultEdge; + +public class CircularReferenceChecker { + + /** + * Detects cycles in the classReferencesGraph parameter + * and stores the cycles of a class as a subgraph in a Map + * + * @param classReferencesGraph + * @return a Map of Class and its Cycle Graph + */ + public Map> detectCycles(Graph classReferencesGraph) { + Map> cyclesForEveryVertexMap = new HashMap<>(); + CycleDetector cycleDetector = new CycleDetector<>(classReferencesGraph); + cycleDetector.findCycles().forEach(v -> { + AsSubgraph subGraph = + new AsSubgraph<>(classReferencesGraph, cycleDetector.findCyclesContainingVertex(v)); + cyclesForEveryVertexMap.put(v, subGraph); + }); + return cyclesForEveryVertexMap; + } + + /** + * Given graph and image name, use jgrapht to create .png file of graph in given outputDirectory. + * Create outputDirectory if it does not exist. + * + * @param outputDirectoryPath + * @param subGraph + * @param imageName + * @throws IOException + */ + public void createImage(String outputDirectoryPath, Graph subGraph, String imageName) + throws IOException { + new File(outputDirectoryPath).mkdirs(); + File imgFile = new File(outputDirectoryPath + "/graph" + imageName + ".png"); + if (imgFile.createNewFile()) { + JGraphXAdapter graphAdapter = new JGraphXAdapter<>(subGraph); + mxIGraphLayout layout = new mxCircleLayout(graphAdapter); + layout.execute(graphAdapter.getDefaultParent()); + + BufferedImage image = mxCellRenderer.createBufferedImage(graphAdapter, null, 2, Color.WHITE, true, null); + if (image != null) { + ImageIO.write(image, "PNG", imgFile); + } + } + } +} diff --git a/circular-reference-detector/src/main/java/org/hjug/parser/JavaProjectParser.java b/circular-reference-detector/src/main/java/org/hjug/parser/JavaProjectParser.java new file mode 100644 index 00000000..b55f40f7 --- /dev/null +++ b/circular-reference-detector/src/main/java/org/hjug/parser/JavaProjectParser.java @@ -0,0 +1,130 @@ +package org.hjug.parser; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; + +public class JavaProjectParser { + + /** + * Given a java source directory return a graph of class references + * @param srcDirectory + * @return + * @throws IOException + */ + public Graph getClassReferences(String srcDirectory) throws IOException { + Graph classReferencesGraph = new DefaultDirectedGraph<>(DefaultEdge.class); + if (srcDirectory == null || srcDirectory.isEmpty()) { + throw new IllegalArgumentException(); + } else { + List classNames = getClassNames(srcDirectory); + try (Stream filesStream = Files.walk(Paths.get(srcDirectory))) { + filesStream + .filter(path -> path.getFileName().toString().endsWith(".java")) + .forEach(path -> { + Set varTypes = getInstanceVarTypes(classNames, path.toFile()); + varTypes.addAll(getMethodTypes(classNames, path.toFile())); + if (!varTypes.isEmpty()) { + String className = + getClassName(path.getFileName().toString()); + classReferencesGraph.addVertex(className); + varTypes.forEach(classReferencesGraph::addVertex); + varTypes.forEach(var -> classReferencesGraph.addEdge(className, var)); + } + }); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + return classReferencesGraph; + } + + /** + * Get instance variables types of a java source file using java parser + * @param classNamesToFilterBy - only add instance variable types which have these class names as type + * @param file + * @return + */ + private Set getInstanceVarTypes(List classNamesToFilterBy, File javaSrcFile) { + CompilationUnit compilationUnit; + try { + compilationUnit = StaticJavaParser.parse(javaSrcFile); + return compilationUnit.findAll(FieldDeclaration.class).stream() + .map(f -> f.getVariables().get(0).getType()) + .filter(v -> !v.isPrimitiveType()) + .map(Object::toString) + .filter(classNamesToFilterBy::contains) + .collect(Collectors.toSet()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return new HashSet<>(); + } + + /** + * Get parameter types of methods declared in a java source file using java parser + * @param classNamesToFilterBy - only add types which have these class names as type + * @param file + * @return + */ + private Set getMethodTypes(List classNamesToFilterBy, File javaSrcFile) { + CompilationUnit compilationUnit; + try { + compilationUnit = StaticJavaParser.parse(javaSrcFile); + return compilationUnit.findAll(MethodDeclaration.class).stream() + .flatMap(f -> f.getParameters().stream() + .map(Parameter::getType) + .filter(type -> !type.isPrimitiveType()) + .collect(Collectors.toSet()) + .stream()) + .map(Object::toString) + .filter(classNamesToFilterBy::contains) + .collect(Collectors.toSet()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return new HashSet<>(); + } + + /** + * Get all java classes in a source directory + * + * @param srcDirectory + * @return + * @throws IOException + */ + private List getClassNames(String srcDirectory) throws IOException { + try (Stream filesStream = Files.walk(Paths.get(srcDirectory))) { + return filesStream + .map(path -> path.getFileName().toString()) + .filter(fileName -> fileName.endsWith(".java")) + .map(this::getClassName) + .collect(Collectors.toList()); + } + } + + /** + * Extract class name from java file name + * Example : MyJavaClass.java becomes MyJavaClass + * + * @param javaFileName + * @return + */ + private String getClassName(String javaFileName) { + return javaFileName.substring(0, javaFileName.indexOf('.')); + } +} diff --git a/circular-reference-detector/src/test/java/org/hjug/cycledetector/CircularReferenceCheckerTests.java b/circular-reference-detector/src/test/java/org/hjug/cycledetector/CircularReferenceCheckerTests.java new file mode 100644 index 00000000..2a64f74d --- /dev/null +++ b/circular-reference-detector/src/test/java/org/hjug/cycledetector/CircularReferenceCheckerTests.java @@ -0,0 +1,50 @@ +package org.hjug.cycledetector; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import org.jgrapht.Graph; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CircularReferenceCheckerTests { + + CircularReferenceChecker sutCircularReferenceChecker = new CircularReferenceChecker(); + + @DisplayName("Detect 3 cycles from given graph.") + @Test + public void detectCyclesTest() { + Graph classReferencesGraph = new DefaultDirectedGraph<>(DefaultEdge.class); + classReferencesGraph.addVertex("A"); + classReferencesGraph.addVertex("B"); + classReferencesGraph.addVertex("C"); + classReferencesGraph.addEdge("A", "B"); + classReferencesGraph.addEdge("B", "C"); + classReferencesGraph.addEdge("C", "A"); + Map> cyclesForEveryVertexMap = + sutCircularReferenceChecker.detectCycles(classReferencesGraph); + assertEquals(3, cyclesForEveryVertexMap.size()); + } + + @DisplayName("Create graph image in given outputDirectory") + @Test + public void createImageTest() throws IOException { + Graph classReferencesGraph = new DefaultDirectedGraph<>(DefaultEdge.class); + classReferencesGraph.addVertex("A"); + classReferencesGraph.addVertex("B"); + classReferencesGraph.addVertex("C"); + classReferencesGraph.addEdge("A", "B"); + classReferencesGraph.addEdge("B", "C"); + classReferencesGraph.addEdge("C", "A"); + sutCircularReferenceChecker.createImage( + "src/test/resources/testOutputDirectory", classReferencesGraph, "testGraph"); + File newGraphImage = new File("src/test/resources/testOutputDirectory/graphtestGraph.png"); + assertTrue(newGraphImage.exists() && !newGraphImage.isDirectory()); + } +} diff --git a/circular-reference-detector/src/test/java/org/hjug/parser/JavaProjectParserTests.java b/circular-reference-detector/src/test/java/org/hjug/parser/JavaProjectParserTests.java new file mode 100644 index 00000000..34e3c428 --- /dev/null +++ b/circular-reference-detector/src/test/java/org/hjug/parser/JavaProjectParserTests.java @@ -0,0 +1,46 @@ +package org.hjug.parser; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class JavaProjectParserTests { + + JavaProjectParser sutJavaProjectParser = new JavaProjectParser(); + + @DisplayName("When source directory input param is empty or null throw IllegalArgumentException.") + @Test + public void parseSourceDirectoryEmptyTest() { + Assertions.assertThrows(IllegalArgumentException.class, () -> sutJavaProjectParser.getClassReferences("")); + Assertions.assertThrows(IllegalArgumentException.class, () -> sutJavaProjectParser.getClassReferences(null)); + } + + @DisplayName("Given a valid source directory input parameter return a valid graph.") + @Test + public void parseSourceDirectoryTest() throws IOException { + File srcDirectory = new File("src/test/resources/javaSrcDirectory"); + Graph classReferencesGraph = + sutJavaProjectParser.getClassReferences(srcDirectory.getAbsolutePath()); + assertNotNull(classReferencesGraph); + assertEquals(5, classReferencesGraph.vertexSet().size()); + assertEquals(7, classReferencesGraph.edgeSet().size()); + assertTrue(classReferencesGraph.containsVertex("A")); + assertTrue(classReferencesGraph.containsVertex("B")); + assertTrue(classReferencesGraph.containsVertex("C")); + assertTrue(classReferencesGraph.containsVertex("D")); + assertTrue(classReferencesGraph.containsVertex("E")); + assertTrue(classReferencesGraph.containsEdge("A", "B")); + assertTrue(classReferencesGraph.containsEdge("B", "C")); + assertTrue(classReferencesGraph.containsEdge("C", "A")); + assertTrue(classReferencesGraph.containsEdge("C", "E")); + assertTrue(classReferencesGraph.containsEdge("D", "A")); + assertTrue(classReferencesGraph.containsEdge("D", "C")); + assertTrue(classReferencesGraph.containsEdge("E", "D")); + } +} diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java new file mode 100644 index 00000000..57de3bda --- /dev/null +++ b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java @@ -0,0 +1,5 @@ +package com.ideacrest.parser.testclasses; + +public class A { + B b; +} diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java new file mode 100644 index 00000000..7dd0ee11 --- /dev/null +++ b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java @@ -0,0 +1,5 @@ +package com.ideacrest.parser.testclasses; + +public class B { + C c; +} diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java new file mode 100644 index 00000000..cb5bb45c --- /dev/null +++ b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java @@ -0,0 +1,6 @@ +package com.ideacrest.parser.testclasses; + +public class C { + A a; + E e; +} diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java new file mode 100644 index 00000000..5c7e265a --- /dev/null +++ b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java @@ -0,0 +1,6 @@ +package com.ideacrest.parser.testclasses; + +public class D { + A a; + C c; +} diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java new file mode 100644 index 00000000..5b9fb6e5 --- /dev/null +++ b/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java @@ -0,0 +1,5 @@ +package com.ideacrest.parser.testclasses; + +public class E { + D d; +} diff --git a/circular-reference-detector/src/test/resources/testOutputDirectory/graphtestGraph.png b/circular-reference-detector/src/test/resources/testOutputDirectory/graphtestGraph.png new file mode 100644 index 00000000..811aea4c Binary files /dev/null and b/circular-reference-detector/src/test/resources/testOutputDirectory/graphtestGraph.png differ diff --git a/cost-benefit-calculator/pom.xml b/cost-benefit-calculator/pom.xml index b8dbff91..15b9e4c0 100644 --- a/cost-benefit-calculator/pom.xml +++ b/cost-benefit-calculator/pom.xml @@ -29,6 +29,11 @@ effort-ranker + + org.hjug.refactorfirst.circularreferencedetector + circular-reference-detector + + org.hjug.refactorfirst.testresources test-resources 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 0654fb29..5a49fdb9 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 @@ -16,20 +16,30 @@ 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; import org.hjug.git.ScmLogInfo; import org.hjug.metrics.*; import org.hjug.metrics.rules.CBORule; +import org.hjug.parser.JavaProjectParser; +import org.jgrapht.Graph; +import org.jgrapht.alg.flow.GusfieldGomoryHuCutTree; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.AsUndirectedGraph; +import org.jgrapht.graph.DefaultEdge; @Slf4j public class CostBenefitCalculator { + private final Map renderedSubGraphs = new HashMap<>(); + private Report report; private String repositoryPath; private final GitLogReader gitLogReader = new GitLogReader(); private Repository repository = null; private final ChangePronenessRanker changePronenessRanker; + private final JavaProjectParser javaProjectParser = new JavaProjectParser(); public CostBenefitCalculator(String repositoryPath) { this.repositoryPath = repositoryPath; @@ -48,6 +58,100 @@ public CostBenefitCalculator(String repositoryPath) { changePronenessRanker = new ChangePronenessRanker(repository, gitLogReader); } + public List runCycleAnalysis() { + List rankedCycles = new ArrayList<>(); + try { + Map classNamesAndPaths = getClassNamesAndPaths(); + Graph classReferencesGraph = javaProjectParser.getClassReferences(repositoryPath); + CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker(); + Map> cyclesForEveryVertexMap = + circularReferenceChecker.detectCycles(classReferencesGraph); + cyclesForEveryVertexMap.forEach((vertex, subGraph) -> { + int vertexCount = subGraph.vertexSet().size(); + int edgeCount = subGraph.edgeSet().size(); + double minCut = 0; + Set minCutEdges = null; + if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) { + // circularReferenceChecker.createImage(outputDirectoryPath, subGraph, vertex); + renderedSubGraphs.put(vertex, subGraph); + log.info("Vertex: " + vertex + " vertex count: " + vertexCount + " edge count: " + edgeCount); + GusfieldGomoryHuCutTree gusfieldGomoryHuCutTree = + new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph)); + minCut = gusfieldGomoryHuCutTree.calculateMinCut(); + log.info("Min cut weight: " + minCut); + minCutEdges = gusfieldGomoryHuCutTree.getCutEdges(); + + log.info("Minimum Cut Edges:"); + for (DefaultEdge minCutEdge : minCutEdges) { + log.info(minCutEdge.toString()); + } + } + + List cycleNodes = subGraph.vertexSet().stream() + .map(classInCycle -> new CycleNode(classInCycle, classNamesAndPaths.get(classInCycle))) + .collect(Collectors.toList()); + List changeRanks = getRankedChangeProneness(cycleNodes); + + Map cycleNodeMap = new HashMap<>(); + + for (CycleNode cycleNode : cycleNodes) { + cycleNodeMap.put(cycleNode.getFileName(), cycleNode); + } + + for (ScmLogInfo changeRank : changeRanks) { + CycleNode cn = cycleNodeMap.get(changeRank.getPath()); + cn.setScmLogInfo(changeRank); + } + + // sum change proneness ranks + int changePronenessRankSum = changeRanks.stream() + .mapToInt(ScmLogInfo::getChangePronenessRank) + .sum(); + rankedCycles.add(new RankedCycle( + vertex, + changePronenessRankSum, + subGraph.vertexSet(), + subGraph.edgeSet(), + minCut, + minCutEdges, + cycleNodes)); + }); + + rankedCycles.sort(Comparator.comparing(RankedCycle::getAverageChangeProneness)); + int cpr = 1; + for (RankedCycle rankedCycle : rankedCycles) { + rankedCycle.setChangePronenessRank(cpr++); + } + + rankedCycles.sort(Comparator.comparing(RankedCycle::getRawPriority).reversed()); + + int priority = 1; + for (RankedCycle rankedCycle : rankedCycles) { + rankedCycle.setPriority(priority++); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + + return rankedCycles; + } + + private boolean isDuplicateSubGraph(AsSubgraph subGraph, String vertex) { + if (!renderedSubGraphs.isEmpty()) { + for (AsSubgraph renderedSubGraph : renderedSubGraphs.values()) { + if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size() + && renderedSubGraph.edgeSet().size() + == subGraph.edgeSet().size() + && renderedSubGraph.vertexSet().contains(vertex)) { + return true; + } + } + } + + return false; + } + // copied from PMD's PmdTaskImpl.java and modified public void runPmdAnalysis() throws IOException { PMDConfiguration configuration = new PMDConfiguration(); @@ -186,4 +290,31 @@ private List getCBOClasses() { private String getFileName(RuleViolation violation) { return violation.getFileId().getUriString().replace("file:///" + repositoryPath.replace("\\", "/") + "/", ""); } + + public Map getClassNamesAndPaths() throws IOException { + + Map fileNamePaths = new HashMap<>(); + + Files.walk(Paths.get(repositoryPath)).forEach(path -> { + String filename = path.getFileName().toString(); + if (filename.endsWith(".java")) { + fileNamePaths.put( + getClassName(filename), + path.toUri().toString().replace("file:///" + repositoryPath.replace("\\", "/") + "/", "")); + } + }); + + return fileNamePaths; + } + + /** + * Extract class name from java file name + * Example : MyJavaClass.java becomes MyJavaClass + * + * @param javaFileName + * @return + */ + private String getClassName(String javaFileName) { + return javaFileName.substring(0, javaFileName.indexOf('.')); + } } diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleNode.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleNode.java new file mode 100644 index 00000000..a00e6635 --- /dev/null +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleNode.java @@ -0,0 +1,24 @@ +package org.hjug.cbc; + +import java.time.Instant; +import lombok.Data; +import org.hjug.git.ScmLogInfo; +import org.hjug.metrics.Disharmony; + +@Data +public class CycleNode implements Disharmony { + + private final String className; + private final String fileName; + private Integer changePronenessRank; + + private Instant firstCommitTime; + private Instant mostRecentCommitTime; + private Integer commitCount; + + public void setScmLogInfo(ScmLogInfo scmLogInfo) { + firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit()); + mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit()); + commitCount = scmLogInfo.getCommitCount(); + } +} diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedCycle.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedCycle.java new file mode 100644 index 00000000..44b5bca0 --- /dev/null +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedCycle.java @@ -0,0 +1,59 @@ +package org.hjug.cbc; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.graph.DefaultEdge; + +@Data +@Slf4j +public class RankedCycle { + + private final String cycleName; + private final Integer changePronenessRankSum; + + private final Set vertexSet; + private final Set edgeSet; + private final double minCutCount; + private final Set minCutEdges; + private final List cycleNodes; + + private float rawPriority; + private Integer priority = 0; + private float averageChangeProneness; + private Integer changePronenessRank; + private float impact; + + public RankedCycle( + String cycleName, + Integer changePronenessRankSum, + Set vertexSet, + Set edgeSet, + double minCutCount, + Set minCutEdges, + List cycleNodes) { + this.cycleNodes = cycleNodes; + this.cycleName = cycleName; + this.changePronenessRankSum = changePronenessRankSum; + this.vertexSet = vertexSet; + this.edgeSet = edgeSet; + this.minCutCount = minCutCount; + + if (null == minCutEdges) { + this.minCutEdges = new HashSet<>(); + } else { + this.minCutEdges = minCutEdges; + } + + if (minCutCount == 0.0) { + this.impact = (float) (vertexSet.size()); + } else { + this.impact = (float) (vertexSet.size() / minCutCount); + } + + this.averageChangeProneness = (float) changePronenessRankSum / vertexSet.size(); + this.rawPriority = this.impact + averageChangeProneness; + } +} diff --git a/pom.xml b/pom.xml index 1da13a12..27d1f421 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ test-resources + circular-reference-detector change-proneness-ranker effort-ranker cost-benefit-calculator @@ -99,6 +100,12 @@ ${project.version} + + org.hjug.refactorfirst.circularreferencedetector + circular-reference-detector + ${project.version} + + org.hjug.refactorfirst.costbenefitcalculator cost-benefit-calculator 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 3bdd5742..c99637c3 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java @@ -14,6 +14,7 @@ import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.hjug.cbc.CostBenefitCalculator; +import org.hjug.cbc.RankedCycle; import org.hjug.cbc.RankedDisharmony; import org.hjug.git.GitLogReader; @@ -77,6 +78,10 @@ public class SimpleHtmlReport { "Class", "Priority", "Change Proneness Rank", "Coupling Count", "Most Recent Commit Date", "Commit Count" }; + public final String[] cycleTableHeadings = { + "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Minimum Cuts" + }; + public void execute( boolean showDetails, String projectName, String projectVersion, String outputDirectory, File baseDir) { @@ -150,8 +155,8 @@ public void execute( throw new RuntimeException(e); } List rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); - List rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); + List rankedCycles = costBenefitCalculator.runCycleAnalysis(); if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty()) { stringBuilder @@ -159,7 +164,7 @@ public void execute( .append(projectName) .append(" ") .append(projectVersion) - .append(" has no God classes or highly coupled classes!"); + .append(" has no God classes, highly coupled classes, or cycles!"); renderGithubButtons(stringBuilder); log.info("Done! No Disharmonies found!"); stringBuilder.append(THE_END); @@ -174,125 +179,201 @@ public void execute( } if (!rankedGodClassDisharmonies.isEmpty()) { - int maxGodClassPriority = rankedGodClassDisharmonies - .get(rankedGodClassDisharmonies.size() - 1) - .getPriority(); + renderGodClassInfo( + showDetails, + outputDirectory, + rankedGodClassDisharmonies, + stringBuilder, + godClassTableHeadings, + formatter); + } - stringBuilder.append(""); + if (!rankedGodClassDisharmonies.isEmpty() && !rankedCBODisharmonies.isEmpty()) { + stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
"); + } - renderGodClassChart(outputDirectory, rankedGodClassDisharmonies, maxGodClassPriority, stringBuilder); + if (!rankedCBODisharmonies.isEmpty()) { + renderHighlyCoupledClassInfo(outputDirectory, stringBuilder, rankedCBODisharmonies, formatter); + } - stringBuilder.append( - "

God classes by the numbers: (Refactor Starting with Priority 1)

"); - stringBuilder.append(""); + if (!rankedCycles.isEmpty()) { + renderCycles(outputDirectory, stringBuilder, rankedCycles, formatter); + } - // Content - stringBuilder.append(""); - for (String heading : godClassTableHeadings) { - stringBuilder.append(""); - } - stringBuilder.append(""); - - stringBuilder.append(""); - for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { - stringBuilder.append(""); - - String[] simpleRankedGodClassDisharmonyData = { - rankedGodClassDisharmony.getFileName(), - rankedGodClassDisharmony.getPriority().toString(), - rankedGodClassDisharmony.getChangePronenessRank().toString(), - rankedGodClassDisharmony.getEffortRank().toString(), - rankedGodClassDisharmony.getWmc().toString(), - formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), - rankedGodClassDisharmony.getCommitCount().toString() - }; - - String[] detailedRankedGodClassDisharmonyData = { - rankedGodClassDisharmony.getFileName(), - rankedGodClassDisharmony.getPriority().toString(), - rankedGodClassDisharmony.getRawPriority().toString(), - rankedGodClassDisharmony.getChangePronenessRank().toString(), - rankedGodClassDisharmony.getEffortRank().toString(), - rankedGodClassDisharmony.getWmc().toString(), - rankedGodClassDisharmony.getWmcRank().toString(), - rankedGodClassDisharmony.getAtfd().toString(), - rankedGodClassDisharmony.getAtfdRank().toString(), - rankedGodClassDisharmony.getTcc().toString(), - rankedGodClassDisharmony.getTccRank().toString(), - formatter.format(rankedGodClassDisharmony.getFirstCommitTime()), - formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), - rankedGodClassDisharmony.getCommitCount().toString(), - rankedGodClassDisharmony.getPath() - }; - - final String[] rankedDisharmonyData = - showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData; - - for (String rowData : rankedDisharmonyData) { - drawTableCell(rowData, stringBuilder); - } - - stringBuilder.append(""); + stringBuilder.append(""); + printProjectFooter(stringBuilder, formatter); + stringBuilder.append(THE_END); + + log.debug(stringBuilder.toString()); + + writeReportToDisk(outputDirectory, filename, stringBuilder); + log.info("Done! View the report at target/site/{}", filename); + } + + private void renderCycles( + String outputDirectory, + StringBuilder stringBuilder, + List rankedCycles, + DateTimeFormatter formatter) { + + stringBuilder.append(""); + + stringBuilder.append( + "

Class Cycles by the numbers: (Refactor starting with Priority 1)

"); + stringBuilder.append("
").append(heading).append("
"); + + // Content + stringBuilder.append(""); + for (String heading : cycleTableHeadings) { + stringBuilder.append(""); + } + + stringBuilder.append(""); + for (RankedCycle rankedCboClassDisharmony : rankedCycles) { + stringBuilder.append(""); + + // "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Min Cuts" + String[] rankedCycleData = { + rankedCboClassDisharmony.getCycleName(), + rankedCboClassDisharmony.getPriority().toString(), + rankedCboClassDisharmony.getChangePronenessRank().toString(), + String.valueOf(rankedCboClassDisharmony.getCycleNodes().size()), + String.valueOf(rankedCboClassDisharmony.getEdgeSet().size()), + rankedCboClassDisharmony.getMinCutEdges().toString() + }; + + for (String rowData : rankedCycleData) { + drawTableCell(rowData, stringBuilder); } - stringBuilder.append(""); - stringBuilder.append("
").append(heading).append("
"); + stringBuilder.append(""); } - if (!rankedCBODisharmonies.isEmpty()) { + stringBuilder.append(""); - stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
"); + stringBuilder.append(""); - stringBuilder.append( - ""); + stringBuilder.append(""); + } - int maxCboPriority = - rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); + private void renderGodClassInfo( + boolean showDetails, + String outputDirectory, + List rankedGodClassDisharmonies, + StringBuilder stringBuilder, + String[] godClassTableHeadings, + DateTimeFormatter formatter) { + int maxGodClassPriority = rankedGodClassDisharmonies + .get(rankedGodClassDisharmonies.size() - 1) + .getPriority(); - renderCBOChart(outputDirectory, rankedCBODisharmonies, maxCboPriority, stringBuilder); + stringBuilder.append(""); - stringBuilder.append( - "

Highly Coupled classes by the numbers: (Refactor starting with Priority 1)

"); - stringBuilder.append(""); + renderGodClassChart(outputDirectory, rankedGodClassDisharmonies, maxGodClassPriority, stringBuilder); - // Content - stringBuilder.append(""); - for (String heading : cboTableHeadings) { - stringBuilder.append(""); - } - stringBuilder.append(""); - - stringBuilder.append(""); - for (RankedDisharmony rankedCboClassDisharmony : rankedCBODisharmonies) { - stringBuilder.append(""); - - String[] rankedCboClassDisharmonyData = { - rankedCboClassDisharmony.getFileName(), - rankedCboClassDisharmony.getPriority().toString(), - rankedCboClassDisharmony.getChangePronenessRank().toString(), - rankedCboClassDisharmony.getEffortRank().toString(), - formatter.format(rankedCboClassDisharmony.getMostRecentCommitTime()), - rankedCboClassDisharmony.getCommitCount().toString() - }; - - for (String rowData : rankedCboClassDisharmonyData) { - drawTableCell(rowData, stringBuilder); - } - - stringBuilder.append(""); + stringBuilder.append( + "

God classes by the numbers: (Refactor Starting with Priority 1)

"); + stringBuilder.append("
").append(heading).append("
"); + + // Content + stringBuilder.append(""); + for (String heading : godClassTableHeadings) { + stringBuilder.append(""); + } + stringBuilder.append(""); + + stringBuilder.append(""); + for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { + stringBuilder.append(""); + + String[] simpleRankedGodClassDisharmonyData = { + rankedGodClassDisharmony.getFileName(), + rankedGodClassDisharmony.getPriority().toString(), + rankedGodClassDisharmony.getChangePronenessRank().toString(), + rankedGodClassDisharmony.getEffortRank().toString(), + rankedGodClassDisharmony.getWmc().toString(), + formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), + rankedGodClassDisharmony.getCommitCount().toString() + }; + + String[] detailedRankedGodClassDisharmonyData = { + rankedGodClassDisharmony.getFileName(), + rankedGodClassDisharmony.getPriority().toString(), + rankedGodClassDisharmony.getRawPriority().toString(), + rankedGodClassDisharmony.getChangePronenessRank().toString(), + rankedGodClassDisharmony.getEffortRank().toString(), + rankedGodClassDisharmony.getWmc().toString(), + rankedGodClassDisharmony.getWmcRank().toString(), + rankedGodClassDisharmony.getAtfd().toString(), + rankedGodClassDisharmony.getAtfdRank().toString(), + rankedGodClassDisharmony.getTcc().toString(), + rankedGodClassDisharmony.getTccRank().toString(), + formatter.format(rankedGodClassDisharmony.getFirstCommitTime()), + formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()), + rankedGodClassDisharmony.getCommitCount().toString(), + rankedGodClassDisharmony.getPath() + }; + + final String[] rankedDisharmonyData = + showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData; + + for (String rowData : rankedDisharmonyData) { + drawTableCell(rowData, stringBuilder); } - stringBuilder.append(""); + stringBuilder.append(""); } - stringBuilder.append("
").append(heading).append("
"); - printProjectFooter(stringBuilder, formatter); - stringBuilder.append(THE_END); + stringBuilder.append(""); + stringBuilder.append(""); + } - log.debug(stringBuilder.toString()); + private void renderHighlyCoupledClassInfo( + String outputDirectory, + StringBuilder stringBuilder, + List rankedCBODisharmonies, + DateTimeFormatter formatter) { + stringBuilder.append( + ""); - writeReportToDisk(outputDirectory, filename, stringBuilder); - log.info("Done! View the report at target/site/{}", filename); + int maxCboPriority = + rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); + + renderCBOChart(outputDirectory, rankedCBODisharmonies, maxCboPriority, stringBuilder); + + stringBuilder.append( + "

Highly Coupled classes by the numbers: (Refactor starting with Priority 1)

"); + stringBuilder.append(""); + + // Content + stringBuilder.append(""); + for (String heading : cboTableHeadings) { + stringBuilder.append(""); + } + stringBuilder.append(""); + + stringBuilder.append(""); + for (RankedDisharmony rankedCboClassDisharmony : rankedCBODisharmonies) { + stringBuilder.append(""); + + String[] rankedCboClassDisharmonyData = { + rankedCboClassDisharmony.getFileName(), + rankedCboClassDisharmony.getPriority().toString(), + rankedCboClassDisharmony.getChangePronenessRank().toString(), + rankedCboClassDisharmony.getEffortRank().toString(), + formatter.format(rankedCboClassDisharmony.getMostRecentCommitTime()), + rankedCboClassDisharmony.getCommitCount().toString() + }; + + for (String rowData : rankedCboClassDisharmonyData) { + drawTableCell(rowData, stringBuilder); + } + + stringBuilder.append(""); + } + + stringBuilder.append(""); + stringBuilder.append("
").append(heading).append("
"); } void drawTableCell(String rowData, StringBuilder stringBuilder) {