diff --git a/src/main/java/org/jpeek/App.java b/src/main/java/org/jpeek/App.java index e356ffde..6f258741 100644 --- a/src/main/java/org/jpeek/App.java +++ b/src/main/java/org/jpeek/App.java @@ -49,6 +49,7 @@ import org.cactoos.scalar.IoChecked; import org.cactoos.scalar.LengthOf; import org.jpeek.calculus.Calculus; +import org.jpeek.calculus.java.Ccm; import org.jpeek.calculus.xsl.XslCalculus; import org.jpeek.skeleton.Skeleton; import org.xembly.Directives; @@ -250,7 +251,6 @@ private void buildReport(final Collection layers, final Collection final Base base = new DefaultBase(this.input); final XML skeleton = new Skeleton(base).xml(); final XSL chain = new XSLChain(layers); - final Calculus xsl = new XslCalculus(); this.save(skeleton.toString(), "skeleton.xml"); Arrays.stream(Metrics.values()) .filter( @@ -258,6 +258,12 @@ private void buildReport(final Collection layers, final Collection ) .forEach( metric -> { + final Calculus xsl; + if (metric == Metrics.CCM) { + xsl = new Ccm(); + } else { + xsl = new XslCalculus(); + } if (Objects.nonNull(metric.getSigma())) { reports.add( new XslReport( diff --git a/src/main/java/org/jpeek/calculus/java/Ccm.java b/src/main/java/org/jpeek/calculus/java/Ccm.java index a24123cc..f85db65b 100644 --- a/src/main/java/org/jpeek/calculus/java/Ccm.java +++ b/src/main/java/org/jpeek/calculus/java/Ccm.java @@ -24,17 +24,26 @@ package org.jpeek.calculus.java; import com.jcabi.xml.XML; +import com.jcabi.xml.XMLDocument; import com.jcabi.xml.XSLDocument; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import org.cactoos.io.ResourceOf; import org.cactoos.io.UncheckedInput; import org.cactoos.text.FormattedText; -import org.cactoos.text.Joined; import org.jpeek.calculus.Calculus; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; /** * CCM metric Java calculus. + * This class implements the Calculus interface to provide functionality + * for computing the CCM metric for Java code. * @since 0.30.25 */ public final class Ccm implements Calculus { @@ -52,57 +61,163 @@ public XML node( ).toString() ); } - return Ccm.withFixedNcc( - new XSLDocument( - new UncheckedInput( - new ResourceOf("org/jpeek/metrics/CCM.xsl") - ).stream() - ).transform(skeleton), - skeleton + final XSLDocument doc = new XSLDocument( + new UncheckedInput( + new ResourceOf("org/jpeek/metrics/CCM.xsl") + ).stream() ); + final XML meta = addMetaInformation(skeleton, params); + return doc.transform(meta); } /** - * Updates the transformed xml with proper NCC value. - * @param transformed The transformed XML skeleton. - * @param skeleton XML Skeleton - * @return XML with fixed NCC. + * Adds meta information to the skeleton XML document. + * This method modifies the skeleton XML document by adding meta information + * about the computed CCM metric. + * @param skeleton The skeleton XML document representing the code structure. + * @param params Parameters for the computation. + * @return The modified XML document containing meta information. */ - private static XML withFixedNcc(final XML transformed, final XML skeleton) { - final List packages = transformed.nodes("//package"); - for (final XML elt : packages) { - final String pack = elt.xpath("/@id").get(0); - final List classes = elt.nodes("//class"); - for (final XML clazz : classes) { - Ccm.updateNcc(skeleton, pack, clazz); + private static XML addMetaInformation(final XML skeleton, final Map params) { + try { + final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() + .newDocument(); + final Element meta = doc.createElement("meta"); + final List packages = skeleton.nodes("//package"); + for (final XML pack : packages) { + final Element tag = doc.createElement("package"); + tag.setAttribute( + "id", pack.node().getAttributes().getNamedItem("id").getNodeValue() + ); + final List classes = pack.nodes("class"); + for (final XML clazz: classes) { + final Element sub = doc.createElement("class"); + sub.appendChild(addNccTag(doc, clazz, params)); + sub.setAttribute( + "id", + clazz.node().getAttributes().getNamedItem("id").getNodeValue() + ); + tag.appendChild(sub); + } + meta.appendChild(tag); } + final Node repr = skeleton.node(); + final Node text = repr.getFirstChild().getOwnerDocument() + .importNode(doc.createTextNode("\n"), true); + final Node node = repr.getFirstChild().getOwnerDocument() + .importNode(meta, true); + repr.getFirstChild().appendChild(text); + repr.getFirstChild().appendChild(node); + return new XMLDocument(repr); + } catch (final ParserConfigurationException ex) { + throw new IllegalStateException(ex); } - return transformed; } /** - * Updates the xml node of the class with proper NCC value. - * @param skeleton XML Skeleton - * @param pack Package name - * @param clazz Class node in the resulting xml - * @todo #449:30min Implement NCC calculation with `XmlGraph` and use this - * class to fix CCM metric (see issue #449). To do this, this class, once - * it works correctly, should be integrated with XSL based calculuses in - * `XslReport` (see `todo #449` in Calculus). Write a test to make sure - * the metric is calculated correctly. Also, decide whether the - * whole CCM metric should be implemented in Java, or only the NCC part. - * Update this `todo` accordingly. + * Adds NCC (Number of Component Connections) tag to the XML document. + * This method calculates the NCC for a given class and adds it as a tag to the XML document. + * @param doc The XML document to which the NCC tag will be added. + * @param clazz The XML representation of the class. + * @param params Parameters for the computation (unused). + * @return The NCC node. */ - private static void updateNcc( - final XML skeleton, final String pack, final XML clazz + private static Node addNccTag(final Document doc, final XML clazz, + final Map params ) { - throw new UnsupportedOperationException( - new Joined( - "", - skeleton.toString(), - pack, - clazz.toString() - ).toString() - ); + final Element ncc = doc.createElement("ncc"); + ncc.appendChild(doc.createTextNode(calculateComponents(clazz, params).toString())); + return ncc; + } + + /** + * Calculates the number of components for a given class. + * This method calculates the number of components for a class using the Union-Find algorithm. + * @param clazz The XML representation of the class. + * @param params Parameters for the computation. + * @return The number of components. + */ + private static Integer calculateComponents(final XML clazz, final Map params) { + final Map> connections = new HashMap<>(); + final Map parents = new HashMap<>(); + for (final XML method : clazz.nodes("methods/method")) { + if (!params.containsKey("include-static-methods") + && method.node().getAttributes().getNamedItem("static").getNodeValue() + .equals("true")) { + continue; + } + final String name = method.node().getAttributes().getNamedItem("name").getNodeValue(); + if (!params.containsKey("include-ctors") && name.equals("")) { + continue; + } + parents.put(name, name); + final List ops = method.nodes("ops/op"); + for (final XML operation : ops) { + final String var = operation.node().getTextContent(); + if (connections.containsKey(var)) { + connections.get(var).add(name); + } else { + final List init = new ArrayList<>(0); + init.add(name); + connections.put(var, init); + } + } + } + return unionFind(parents, connections); + } + + /** + * Performs the Union-Find algorithm to calculate the number of components. + * This method implements the Union-Find algorithm to calculate the number of components. + * @param parents The map representing the parent relationship. + * @param connections The map representing the connections between variables and methods. + * @return The number of components. + */ + private static Integer unionFind(final Map parents, + final Map> connections + ) { + int answer = parents.size(); + for (final List conns : connections.values()) { + final String initial = conns.get(0); + for (final String connectable : conns) { + if (!parents.get(initial).equals(parents.get(connectable))) { + answer -= 1; + } + unite(initial, connectable, parents); + } + } + return answer; + } + + /** + * Gets the parent of a node using the Union-Find algorithm. + * This method retrieves the parent of a node using the Union-Find algorithm. + * @param node The node whose parent is to be found. + * @param parents The map representing the parent relationship. + * @return The parent of the node. + */ + private static String getParent(final String node, final Map parents) { + String ancestor = node; + while (!parents.get(ancestor).equals(ancestor)) { + ancestor = parents.get(ancestor); + } + return ancestor; + } + + /** + * Unites two nodes using the Union-Find algorithm. + * This method unites two nodes using the Union-Find algorithm. + * @param node The first node. + * @param son The second node. + * @param parents The map representing the parent relationship. + */ + private static void unite(final String node, final String son, + final Map parents + ) { + final String root = getParent(node, parents); + final String attachable = getParent(son, parents); + if (!root.equals(attachable)) { + parents.put(attachable, root); + } } } diff --git a/src/main/resources/org/jpeek/metrics/CCM.xsl b/src/main/resources/org/jpeek/metrics/CCM.xsl index df0eeb06..72d43180 100644 --- a/src/main/resources/org/jpeek/metrics/CCM.xsl +++ b/src/main/resources/org/jpeek/metrics/CCM.xsl @@ -23,6 +23,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> + @@ -40,7 +41,11 @@ SOFTWARE. + + + + @@ -68,52 +73,9 @@ SOFTWARE. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -147,23 +109,4 @@ SOFTWARE. - - - - - - - - - - - - - - - - - - - diff --git a/src/test/java/org/jpeek/metrics/CcmTest.java b/src/test/java/org/jpeek/metrics/CcmTest.java index b95bef55..154e778a 100644 --- a/src/test/java/org/jpeek/metrics/CcmTest.java +++ b/src/test/java/org/jpeek/metrics/CcmTest.java @@ -23,11 +23,17 @@ */ package org.jpeek.metrics; +import com.jcabi.xml.XML; +import java.util.HashMap; +import org.jpeek.FakeBase; +import org.jpeek.calculus.java.Ccm; +import org.jpeek.skeleton.Skeleton; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; /** * Tests to check CCM metric in different links between attributes and methods. + * * @since 0.29 */ final class CcmTest { @@ -35,15 +41,18 @@ final class CcmTest { /** * Class with one method access one attribute have * ncc metric = methods count. + * * @throws Exception */ @Test void manyComponentInClassTest() throws Exception { - final MetricBase.Report report = new MetricBase( - "org/jpeek/metrics/CCM.xsl" - ).transform( - "CcmManyComp" + final XML result = new Ccm().node( + "ccm", new HashMap<>(0), + new Skeleton( + new FakeBase("CcmManyComp") + ).xml() ); + final MetricBase.Report report = new MetricBase.Report("CcmManyComp", result); report.assertVariable("methods", 5); report.assertVariable("nc", 0); report.assertVariable("nmp", 10); @@ -55,15 +64,18 @@ void manyComponentInClassTest() throws Exception { * Class with one method access one attribute and * Ctor with all attributes initialization have the same * metric as without Ctor. + * * @throws Exception */ @Test void manyComponentWithCtorInClassTest() throws Exception { - final MetricBase.Report report = new MetricBase( - "org/jpeek/metrics/CCM.xsl" - ).transform( - "CcmManyCompWithCtor" + final XML result = new Ccm().node( + "ccm", new HashMap<>(0), + new Skeleton( + new FakeBase("CcmManyCompWithCtor") + ).xml() ); + final MetricBase.Report report = new MetricBase.Report("CcmManyCompWithCtor", result); report.assertVariable("methods", 5); report.assertVariable("nc", 0); report.assertVariable("nmp", 10); @@ -73,11 +85,13 @@ void manyComponentWithCtorInClassTest() throws Exception { @Test void oneComponentInClassTest() throws Exception { - final MetricBase.Report report = new MetricBase( - "org/jpeek/metrics/CCM.xsl" - ).transform( - "CcmOneComp" + final XML result = new Ccm().node( + "ccm", new HashMap<>(0), + new Skeleton( + new FakeBase("CcmOneComp") + ).xml() ); + final MetricBase.Report report = new MetricBase.Report("CcmOneComp", result); report.assertVariable("methods", 5); report.assertVariable("nc", 10); report.assertVariable("nmp", 10); @@ -87,20 +101,24 @@ void oneComponentInClassTest() throws Exception { /** * Check ccm metric for mixed usage: attribute usage, methods calls. + * + * @throws Exception * @todo #522:30min there is a 4th step for incorrect calculation: nc * in case of calling one method from another because of * `xsl:if test="$method/ops/op/text()[. = $other/ops/op/text()]"` * method name is not used for creating edge. - * @throws Exception */ @Test @Disabled void mixedCallsInClassTest() throws Exception { - final MetricBase.Report report = new MetricBase( - "org/jpeek/metrics/CCM.xsl" - ).transform( - "CcmMixCallManyComp" + final XML result = new Ccm().node( + "ccm", + new HashMap<>(0), + new Skeleton( + new FakeBase("CcmMixCallManyComp") + ).xml() ); + final MetricBase.Report report = new MetricBase.Report("CcmMixCallManyComp", result); report.assertVariable("methods", 5); report.assertVariable("nc", 2); report.assertVariable("nmp", 10); diff --git a/src/test/java/org/jpeek/web/ReportsTest.java b/src/test/java/org/jpeek/web/ReportsTest.java index 3ea52599..84f1549f 100644 --- a/src/test/java/org/jpeek/web/ReportsTest.java +++ b/src/test/java/org/jpeek/web/ReportsTest.java @@ -33,7 +33,6 @@ import org.cactoos.text.TextOf; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.llorllale.cactoos.matchers.Assertion;