OOMetrics.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.ant;

import com.jeantessier.classreader.AggregatingClassfileLoader;
import com.jeantessier.classreader.ClassfileLoader;
import com.jeantessier.classreader.LoadListenerVisitorAdapter;
import com.jeantessier.classreader.TransientClassfileLoader;
import com.jeantessier.metrics.*;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.xml.sax.SAXException;

import javax.xml.parsers.*;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.*;
import java.util.*;

public class OOMetrics extends Task {
    public static final String DEFAULT_PROJECT_NAME = "Project";
    public static final String DEFAULT_SORT = "name";

    private String projectName = DEFAULT_PROJECT_NAME;
    private File configuration;
    private boolean csv = false;
    private boolean json = false;
    private boolean text = false;
    private boolean xml = false;
    private boolean yaml = false;
    private boolean validate = false;
    private String encoding = com.jeantessier.metrics.XMLPrinter.DEFAULT_ENCODING;
    private String dtdPrefix = com.jeantessier.metrics.XMLPrinter.DEFAULT_DTD_PREFIX;
    private String indentText;
    private boolean projectMetrics = false;
    private boolean groupMetrics = false;
    private boolean classMetrics = false;
    private boolean methodMetrics = false;
    private Path scopeIncludesList;
    private Path scopeExcludesList;
    private Path filterIncludesList;
    private Path filterExcludesList;
    private boolean showAllMetrics = false;
    private boolean showEmptyMetrics = false;
    private boolean showHiddenMeasurements = false;
    private String sort = DEFAULT_SORT;
    private boolean expand = false;
    private boolean reverse = false;
    private boolean enableCrossClassMeasurements = false;
    private File destprefix;
    private Path path;

    public String getProjectname() {
        return projectName;
    }
    
    public void setProjectname(String projectName) {
        this.projectName = projectName;
    }

    public File getConfiguration() {
        return configuration;
    }
    
    public void setConfiguration(File configuration) {
        this.configuration = configuration;
    }

    public boolean getCsv() {
        return csv;
    }

    public void setCsv(boolean csv) {
        this.csv = csv;
    }

    public boolean getJson() {
        return json;
    }

    public void setJson(boolean json) {
        this.json = json;
    }

    public boolean getText() {
        return text;
    }

    public void setText(boolean text) {
        this.text = text;
    }

    public void setTxt(boolean text) {
        setText(text);
    }

    public boolean getXml() {
        return xml;
    }

    public void setXml(boolean xml) {
        this.xml = xml;
    }

    public boolean getYaml() {
        return yaml;
    }

    public void setYaml(boolean yaml) {
        this.yaml = yaml;
    }

    public void setYml(boolean yaml) {
        setYaml(yaml);
    }

    public boolean getValidate() {
        return validate;
    }

    public void setValidate(boolean validate) {
        this.validate = validate;
    }

    public String getEncoding() {
        return encoding;
    }
    
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public String getDtdprefix() {
        return dtdPrefix;
    }
    
    public void setDtdprefix(String dtdPrefix) {
        this.dtdPrefix = dtdPrefix;
    }

    public String getIndenttext() {
        return indentText;
    }
    
    public void setIndenttext(String indentText) {
        this.indentText = indentText;
    }

    public boolean getProjectmetrics() {
        return projectMetrics;
    }
    
    public void setProjectmetrics(boolean projectMetrics) {
        this.projectMetrics = projectMetrics;
    }

    public boolean getGroupmetrics() {
        return groupMetrics;
    }
    
    public void setGroupmetrics(boolean groupMetrics) {
        this.groupMetrics = groupMetrics;
    }

    public boolean getClassmetrics() {
        return classMetrics;
    }
    
    public void setClassmetrics(boolean classMetrics) {
        this.classMetrics = classMetrics;
    }

    public boolean getMethodmetrics() {
        return methodMetrics;
    }
    
    public void setMethodmetrics(boolean methodMetrics) {
        this.methodMetrics = methodMetrics;
    }

    public void setAllmetrics(boolean allMetrics) {
        setProjectmetrics(allMetrics);
        setGroupmetrics(allMetrics);
        setClassmetrics(allMetrics);
        setMethodmetrics(allMetrics);
    }
    
    public Path createScopeincludeslist() {
        if (scopeIncludesList == null) {
            scopeIncludesList = new Path(getProject());
        }

        return scopeIncludesList;
    }
    
    public Path getScopeincludeslist() {
        return scopeIncludesList;
    }
    
    public Path createScopeexcludeslist() {
        if (scopeExcludesList == null) {
            scopeExcludesList = new Path(getProject());
        }

        return scopeExcludesList;
    }
    
    public Path getScopeexcludeslist() {
        return scopeExcludesList;
    }
    
    public Path createFilterincludeslist() {
        if (filterIncludesList == null) {
            filterIncludesList = new Path(getProject());
        }

        return filterIncludesList;
    }
    
    public Path getFilterincludeslist() {
        return filterIncludesList;
    }
    
    public Path createFilterexcludeslist() {
        if (filterExcludesList == null) {
            filterExcludesList = new Path(getProject());
        }

        return filterExcludesList;
    }
    
    public Path getFilterexcludeslist() {
        return filterExcludesList;
    }

    public boolean getShowallmetrics() {
        return showAllMetrics;
    }
    
    public void setShowallmetrics(boolean showAllMetrics) {
        this.showAllMetrics = showAllMetrics;
    }

    public boolean getShowemptymetrics() {
        return showEmptyMetrics;
    }
    
    public void setShowemptymetrics(boolean showEmptyMetrics) {
        this.showEmptyMetrics = showEmptyMetrics;
    }

    public boolean getShowhiddenmeasurements() {
        return showHiddenMeasurements;
    }
    
    public void setShowhiddenmeasurements(boolean showHiddenMeasurements) {
        this.showHiddenMeasurements = showHiddenMeasurements;
    }
    
    public String getSort() {
        return sort;
    }
    
    public void setSort(String sort) {
        this.sort = sort;
    }

    public boolean getExpand() {
        return expand;
    }
    
    public void setExpand(boolean expand) {
        this.expand = expand;
    }

    public boolean getReverse() {
        return reverse;
    }
    
    public void setReverse(boolean reverse) {
        this.reverse = reverse;
    }

    public boolean getEnablecrossclassmeasurements() {
        return enableCrossClassMeasurements;
    }

    public void setEnablecrossclassmeasurements(boolean enableCrossClassMeasurements) {
        this.enableCrossClassMeasurements = enableCrossClassMeasurements;
    }

    public File getDestprefix() {
        return destprefix;
    }
    
    public void setDestprefix(File destprefix) {
        this.destprefix = destprefix;
    }
    
    public Path createPath() {
        if (path == null) {
            path = new Path(getProject());
        }

        return path;
    }
    
    public Path getPath() {
        return path;
    }
    
    public void execute() throws BuildException {
        // first off, make sure that we've got what we need

        if (getConfiguration() == null) {
            throw new BuildException("configuration must be set!");
        }
        
        if (!getConfiguration().exists()) {
            throw new BuildException("configuration does not exist!");
        }
        
        if (!getConfiguration().isFile()) {
            throw new BuildException("configuration is not a file!");
        }

        if (getPath() == null) {
            throw new BuildException("path must be set!");
        }

        if (getDestprefix() == null) {
            throw new BuildException("destprefix must be set!");
        }

        try {
            VerboseListener verboseListener = new VerboseListener(this);
            
            log("Reading configuration ...");
            MetricsFactory factory = new MetricsFactory(getProjectname(), new MetricsConfigurationLoader(getValidate()).load(getConfiguration().getAbsolutePath()));
            
            com.jeantessier.metrics.MetricsGatherer gatherer = new com.jeantessier.metrics.MetricsGatherer(factory);
            if (getScopeincludeslist() != null || getScopeexcludeslist() != null) {
                gatherer.setScopeIncludes(createCollection(getScopeincludeslist(), getScopeexcludeslist()));
            }
            if (getFilterincludeslist() != null || getFilterexcludeslist() != null) {
                gatherer.setFilterIncludes(createCollection(getFilterincludeslist(), getFilterexcludeslist()));
            }
            gatherer.addMetricsListener(verboseListener);

            if (getEnablecrossclassmeasurements()) {
                log("Reading in all classes from path " + getPath());
                ClassfileLoader loader = new AggregatingClassfileLoader();
                loader.addLoadListener(verboseListener);
                loader.load(Arrays.asList(getPath().list()));

                log("Computing metrics ...");
                gatherer.visitClassfiles(loader.getAllClassfiles());
            } else {
                ClassfileLoader loader = new TransientClassfileLoader();
                loader.addLoadListener(verboseListener);
                loader.addLoadListener(new LoadListenerVisitorAdapter(gatherer));

                log("Reading classes and computing metrics as we go ...");
                loader.load(Arrays.asList(getPath().list()));
            }

            if (getShowallmetrics()) {
                gatherer.getMetricsFactory().getAllClassMetrics().forEach(metrics -> gatherer.getMetricsFactory().includeClassMetrics(metrics));
                gatherer.getMetricsFactory().getAllMethodMetrics().forEach(metrics -> gatherer.getMetricsFactory().includeMethodMetrics(metrics));
            }

            if (getCsv()) {
                printCSVFiles(gatherer.getMetricsFactory());
            } else if (getJson()) {
                printJSONFile(gatherer.getMetricsFactory());
            } else if (getText()) {
                printTextFile(gatherer.getMetricsFactory());
            } else if (getXml()) {
                printXMLFile(gatherer.getMetricsFactory());
            } else if (getYaml()) {
                printYAMLFile(gatherer.getMetricsFactory());
            }
        } catch (SAXException | ParserConfigurationException | IOException ex) {
            throw new BuildException(ex);
        }
    }

    private Collection<String> createCollection(Path includes, Path excludes) throws IOException {
        Collection<String> result = new HashSet<>();

        if (includes != null) {
            for (String filename : includes.list()) {
                result.addAll(Files.readAllLines(Paths.get(filename)));
            }
        }
        
        if (excludes != null) {
            for (String filename : excludes.list()) {
                result.removeAll(Files.readAllLines(Paths.get(filename)));
            }
        }
        
        return result;
    }

    private void printCSVFiles(MetricsFactory factory) throws IOException {
        printCSVFile(getProjectmetrics(), "project", factory.getConfiguration().getProjectMeasurements(), factory.getProjectMetrics());
        printCSVFile(getGroupmetrics(),   "groups",  factory.getConfiguration().getGroupMeasurements(),   factory.getGroupMetrics());
        printCSVFile(getClassmetrics(),   "classes", factory.getConfiguration().getClassMeasurements(),   factory.getClassMetrics());
        printCSVFile(getMethodmetrics(),  "methods", factory.getConfiguration().getMethodMeasurements(),  factory.getMethodMetrics());
    }

    private void printCSVFile(boolean flag, String name, List<MeasurementDescriptor> descriptors, Collection<Metrics> metrics) throws IOException {
        if (flag) {
            String filename = getDestprefix().getAbsolutePath() + "_" + name + ".csv";
            log("Saving metrics to " + filename);

            try (PrintWriter out = new PrintWriter(new FileWriter(filename))) {
                printMetrics(descriptors, metrics, CSVPrinter.class, out);
            }
        }
    }

    private void printJSONFile(MetricsFactory factory) throws IOException {
        String filename = getDestprefix().getAbsolutePath() + ".json";
        log("Saving metrics to " + filename);

        try (PrintWriter out = new PrintWriter(new FileWriter(filename))) {
            printMetrics(factory.getProjectMetrics(), new com.jeantessier.metrics.JSONPrinter(out, factory.getConfiguration()));
        }
    }

    private void printTextFile(MetricsFactory factory) throws IOException {
        String filename = getDestprefix().getAbsolutePath() + ".txt";
        log("Saving metrics to " + filename);
        
        try (PrintWriter out = new PrintWriter(new FileWriter(filename))) {
            printTextFile(getProjectmetrics(), "Project metrics", factory.getConfiguration().getProjectMeasurements(), factory.getProjectMetrics(), out);
            printTextFile(getGroupmetrics(),   "Group metrics",   factory.getConfiguration().getGroupMeasurements(),   factory.getGroupMetrics(),   out);
            printTextFile(getClassmetrics(),   "Class metrics",   factory.getConfiguration().getClassMeasurements(),   factory.getClassMetrics(),   out);
            printTextFile(getMethodmetrics(),  "Method metrics",  factory.getConfiguration().getMethodMeasurements(),  factory.getMethodMetrics(),  out);
        }
    }

    private void printTextFile(boolean flag, String label, List<MeasurementDescriptor> descriptors, Collection<Metrics> metrics, PrintWriter out) {
        if (flag) {
            out.println(label);
            out.println(label.replaceAll(".", "-"));
            printMetrics(descriptors, metrics, TextPrinter.class, out);
            out.println();
        }
    }

    private void printXMLFile(MetricsFactory factory) throws IOException {
        String filename = getDestprefix().getAbsolutePath() + ".xml";
        log("Saving metrics to " + filename);

        try (PrintWriter out = new PrintWriter(new FileWriter(filename))) {
            printMetrics(factory.getProjectMetrics(), new com.jeantessier.metrics.XMLPrinter(out, factory.getConfiguration(), getEncoding(), getDtdprefix()));
        }
    }

    private void printYAMLFile(MetricsFactory factory) throws IOException {
        String filename = getDestprefix().getAbsolutePath() + ".yaml";
        log("Saving metrics to " + filename);

        try (PrintWriter out = new PrintWriter(new FileWriter(filename))) {
            printMetrics(factory.getProjectMetrics(), new com.jeantessier.metrics.YAMLPrinter(out, factory.getConfiguration()));
        }
    }

    private void printMetrics(List<MeasurementDescriptor> descriptors, Collection<Metrics> metrics, Class<? extends Printer> clazz, PrintWriter out) {
        try {
            printMetrics(metrics, clazz.getConstructor(PrintWriter.class, List.class).newInstance(out, descriptors));
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
            throw new BuildException(ex);
        }
    }

    private void printMetrics(Collection<Metrics> metrics, Printer printer) {
        printer.setExpandCollectionMeasurements(getExpand());
        printer.setShowEmptyMetrics(getShowemptymetrics());
        printer.setShowHiddenMeasurements(getShowhiddenmeasurements());
        if (getIndenttext() != null) {
            printer.setIndentText(getIndenttext());
        }

        var comparator = new MetricsComparator(getSort());
        if (getReverse()) {
            comparator.reverse();
        }
        printer.setComparator(comparator);

        printer.visitMetrics(metrics);
    }
}