ClassCohesion.java

/*
 *  Copyright (c) 2001-2024, Jean Tessier
 *  All rights reserved.
 *  
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  
 *      * Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *  
 *      * Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *  
 *      * Neither the name of Jean Tessier nor the names of his contributors
 *        may be used to endorse or promote products derived from this software
 *        without specific prior written permission.
 *  
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jeantessier.dependencyfinder.cli;

import com.jeantessier.commandline.CommandLineException;
import com.jeantessier.dependency.ClassNode;
import com.jeantessier.dependency.FeatureNode;
import com.jeantessier.dependency.LCOM4Gatherer;
import com.jeantessier.dependency.Node;
import org.apache.logging.log4j.*;

import java.io.IOException;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

import static java.util.stream.Collectors.*;

public class ClassCohesion extends DependencyGraphCommand {
    private static final String EOL = System.getProperty("line.separator", "\n");

    private static final String DEFAULT_ENCODING = "utf-8";
    private static final String DEFAULT_DTD_PREFIX = "https://jeantessier.github.io/dependency-finder/dtd";
    private static final String DEFAULT_INDENT_TEXT = "    ";

    protected void populateCommandLineSwitches()  {
        super.populateCommandLineSwitches();
        populateCommandLineSwitchesForXMLOutput(DEFAULT_ENCODING, DEFAULT_DTD_PREFIX, DEFAULT_INDENT_TEXT);

        getCommandLine().addToggleSwitch("csv");
        getCommandLine().addToggleSwitch("json");
        getCommandLine().addToggleSwitch("text");
        getCommandLine().addToggleSwitch("txt");
        getCommandLine().addToggleSwitch("xml");
        getCommandLine().addToggleSwitch("yaml");
        getCommandLine().addToggleSwitch("yml");
        getCommandLine().addToggleSwitch("list");
    }

    protected Collection<CommandLineException> parseCommandLine(String[] args) {
        var exceptions = super.parseCommandLine(args);
        int modeSwitch = 0;

        if (getCommandLine().getToggleSwitch("csv")) {
            modeSwitch++;
        }
        if (getCommandLine().getToggleSwitch("json")) {
            modeSwitch++;
        }
        if (getCommandLine().getToggleSwitch("text")) {
            modeSwitch++;
        }
        if (getCommandLine().getToggleSwitch("txt")) {
            modeSwitch++;
        }
        if (getCommandLine().getToggleSwitch("xml")) {
            modeSwitch++;
        }
        if (getCommandLine().getToggleSwitch("yaml")) {
            modeSwitch++;
        }
        if (getCommandLine().getToggleSwitch("yml")) {
            modeSwitch++;
        }
        if (modeSwitch != 1) {
            exceptions.add(new CommandLineException("Must have one and only one of -csv, -json, -text, -txt, -xml, -yaml, or -yml"));
        }

        return exceptions;
    }

    public void doProcessing() throws Exception {
        var gatherer = new LCOM4Gatherer();

        LogManager.getLogger(OOMetrics.class).debug("Reading classes and computing metrics as we go ...");
        getVerboseListener().print("Reading classes and computing metrics as we go ...");
        gatherer.traverseNodes(loadGraph().getPackages().values());

        LogManager.getLogger(OOMetrics.class).debug("Printing results ...");
        getVerboseListener().print("Printing results ...");

        if (getCommandLine().isPresent("csv")) {
            printCSVFiles(gatherer.getResults());
        } else if (getCommandLine().isPresent("json")) {
             printJSONFile(gatherer.getResults());
        } else if (getCommandLine().isPresent("text") || getCommandLine().isPresent("txt")) {
            printTextFile(gatherer.getResults());
        } else if (getCommandLine().isPresent("xml")) {
            printXMLFile(gatherer.getResults());
        } else if (getCommandLine().isPresent("yaml") || getCommandLine().isPresent("yml")) {
             printYAMLFile(gatherer.getResults());
        }

        LogManager.getLogger(OOMetrics.class).debug("Done.");
    }

    private void printCSVFiles(Map<ClassNode, Collection<Collection<FeatureNode>>> results) throws IOException {
        getOut().println("class, LCOM4");
        getOut().println(results.entrySet().stream()
                .map(entry -> entry.getKey().getName() + ", " + entry.getValue().size())
                .collect(joining(EOL)));
    }

    private void printJSONFile(Map<ClassNode, Collection<Collection<FeatureNode>>> results) throws IOException {
        getOut().print("[");
        getOut().print(results.entrySet().stream()
                .map(entry -> entryToJSON(entry.getKey(), entry.getValue()))
                .collect(joining(", ")));
        getOut().println("]");
    }

    private String entryToJSON(ClassNode classNode, Collection<Collection<FeatureNode>> components) {
        var builder = new StringBuilder();

        builder.append("{\"class\": \"").append(classNode.getName()).append("\"");
        builder.append(", \"LCOM4\": ").append(components.size());

        if (components.size() > 1 && getCommandLine().isPresent("list")) {
            builder.append(", \"components\": [");
            builder.append(componentsToJSON(components));
            builder.append("]");
        }
        builder.append("}");

        return builder.toString();
    }

    private String componentsToJSON(Collection<Collection<FeatureNode>> components) {
        return components.stream()
                .map(this::componentToJSON)
                .map(component -> "[" + component + "]")
                .collect(joining(", "));
    }

    private String componentToJSON(Collection<FeatureNode> nodes) {
        return nodes.stream()
                .map(Node::getName)
                .map(name -> "\"" + name + "\"")
                .collect(joining(", "));
    }

    private void printTextFile(Map<ClassNode, Collection<Collection<FeatureNode>>> results) throws IOException {
        getOut().println(results.entrySet().stream()
                .flatMap(entry -> entryToText(entry.getKey(), entry.getValue()))
                .collect(joining(EOL)));
    }

    private Stream<String> entryToText(ClassNode classNode, Collection<Collection<FeatureNode>> components) {
        return Stream.concat(
                Stream.of(classNode.getName() + ": " + components.size()),
                componentsToText(components)
        );
    }

    private Stream<String> componentsToText(Collection<Collection<FeatureNode>> components) {
        if (components.size() > 1 && getCommandLine().isPresent("list")) {
            return Stream.concat(
                    Stream.of(getTextSeparator()),
                    components.stream()
                            .flatMap(component -> Stream.concat(
                                    componentToText(component),
                                    Stream.of(getTextSeparator())
                            ))
            );
        } else {
            return Stream.empty();
        }
    }

    private Stream<String> componentToText(Collection<FeatureNode> nodes) {
        return nodes.stream()
                .map(feature -> feature.getName().substring(feature.getClassNode().getName().length() + 1))
                .map(name -> getIndentText() + name);
    }

    private String getTextSeparator() {
        return getIndentText() + "--------";
    }

    private void printXMLFile(Map<ClassNode, Collection<Collection<FeatureNode>>> results) throws IOException {
        getOut().println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\" ?>");
        getOut().println();
        getOut().println("<!DOCTYPE classes SYSTEM \"" + getDTDPrefix() + "/cohesion.dtd\">");
        getOut().println();
        getOut().println("<classes>");
        getOut().println(results.entrySet().stream()
                .flatMap(entry -> entryToXML(entry.getKey(), entry.getValue()))
                .collect(joining(EOL)));
        getOut().println("</classes>");
    }

    private Stream<String> entryToXML(ClassNode classNode, Collection<Collection<FeatureNode>> components) {
            if (components.size() > 1 && getCommandLine().isPresent("list")) {
                return Stream.of(
                            Stream.of(getIndentText() + "<class name=\"" + classNode.getName() + "\" lcom4=\"" + components.size() + "\">"),
                            componentsToXML(components),
                            Stream.of(getIndentText() + "</class>")
                        )
                        .flatMap(Function.identity());
            } else {
                return Stream.of(getIndentText() + "<class name=\"" + classNode.getName() + "\" lcom4=\"" + components.size() + "\"/>");
            }
    }

    private Stream<String> componentsToXML(Collection<Collection<FeatureNode>> components) {
        return components.stream()
                .flatMap(component -> Stream.of(
                        Stream.of(getIndentText().repeat(2) + "<component>"),
                        componentToXML(component),
                        Stream.of(getIndentText().repeat(2) + "</component>")
                )
                .flatMap(Function.identity()));
    }

    private Stream<String> componentToXML(Collection<FeatureNode> nodes) {
        return nodes.stream()
                .map(Node::getName)
                .map(name -> getIndentText().repeat(3) + "<feature name=\"" + name + "\"/>");
    }

    private void printYAMLFile(Map<ClassNode, Collection<Collection<FeatureNode>>> results) throws IOException {
        getOut().println(results.entrySet().stream()
                .flatMap(entry -> entryToYAML(entry.getKey(), entry.getValue()))
                .collect(joining(EOL)));
    }

    private Stream<String> entryToYAML(ClassNode classNode, Collection<Collection<FeatureNode>> components) {
        return Stream.concat(
                Stream.of(
                        "-",
                        getIndentText().repeat(1) + "name: " + classNode.getName(),
                        getIndentText().repeat(1) + "lcom4: " + components.size()
                ),
                componentsToYAML(components)
        );
    }

    private Stream<String> componentsToYAML(Collection<Collection<FeatureNode>> components) {
        if (components.size() > 1 && getCommandLine().isPresent("list")) {
            return Stream.concat(
                    Stream.of(getIndentText().repeat(1) + "components:"),
                    components.stream()
                            .flatMap(this::componentToYAML));
        } else {
            return Stream.empty();
        }
    }

    private Stream<String> componentToYAML(Collection<FeatureNode> nodes) {
        return Stream.concat(
                Stream.of(getIndentText().repeat(2) + "-"),
                nodes.stream()
                        .map(Node::getName)
                        .map(name -> getIndentText().repeat(3) + "- " + name));
    }

    private String getEncoding() {
        return getCommandLine().getSingleSwitch("encoding");
    }

    private String getDTDPrefix() {
        return getCommandLine().getSingleSwitch("dtd-prefix");
    }

    private String getIndentText() {
        return getCommandLine().getSingleSwitch("indent-text");
    }

    public static void main(String[] args) throws Exception {
        new ClassCohesion().run(args);
    }
}