ListSymbols.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.*;
import com.jeantessier.text.RegularExpressionParser;
import org.apache.logging.log4j.*;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class ListSymbols extends Task {
    private boolean classes = false;
    private boolean fields = false;
    private boolean methods = false;
    private boolean localVariables = false;
    private boolean innerClasses = false;
    private  boolean publicAccessibility = false;
    private  boolean protectedAccessibility = false;
    private  boolean privateAccessibility = false;
    private  boolean packageAccessibility = false;
    private boolean nonPrivateFields = false;
    private boolean finalMethodsOrClasses = false;
    private List<String> includes = Collections.singletonList("//");
    private Path includesList;
    private List<String> excludes = Collections.emptyList();
    private Path excludesList;
    private File destprefix;
    private Path path;
    private boolean csv = false;
    private boolean json = false;
    private boolean text = false;
    private boolean xml = false;
    private boolean yaml = false;
    private String encoding = XMLSymbolPrinter.DEFAULT_ENCODING;
    private String dtdPrefix = XMLSymbolPrinter.DEFAULT_DTD_PREFIX;
    private String indentText = XMLSymbolPrinter.DEFAULT_INDENT_TEXT;

    public boolean getClasses() {
        return classes;
    }
    
    public void setClasses(boolean classes) {
        this.classes = classes;
    }

    public boolean getFields() {
        return fields;
    }
    
    public void setFields(boolean fields) {
        this.fields = fields;
    }

    public boolean getMethods() {
        return methods;
    }
    
    public void setMethods(boolean methods) {
        this.methods = methods;
    }

    public boolean getLocalvariables() {
        return localVariables;
    }

    public void setLocalvariables(boolean localVariables) {
        this.localVariables = localVariables;
    }

    public boolean getInnerclasses() {
        return innerClasses;
    }

    public void setInnerclasses(boolean innerClasses) {
        this.innerClasses = innerClasses;
    }

    public boolean getPublicaccessibility() {
        return publicAccessibility;
    }

    public void setPublicaccessibility(boolean publicAccessibility) {
        this.publicAccessibility = publicAccessibility;
    }

    public boolean getProtectedaccessibility() {
        return protectedAccessibility;
    }

    public void setProtectedaccessibility(boolean protectedAccessibility) {
        this.protectedAccessibility = protectedAccessibility;
    }

    public boolean getPrivateaccessibility() {
        return privateAccessibility;
    }

    public void setPrivateaccessibility(boolean privateAccessibility) {
        this.privateAccessibility = privateAccessibility;
    }

    public boolean getPackageaccessibility() {
        return packageAccessibility;
    }

    public void setPackageaccessibility(boolean packageAccessibility) {
        this.packageAccessibility = packageAccessibility;
    }

    public boolean getNonprivatefields() {
        return nonPrivateFields;
    }

    public void setNonprivatefields(boolean nonPrivateFields) {
        this.nonPrivateFields = nonPrivateFields;
    }

    public boolean getFinalmethodsorclasses() {
        return finalMethodsOrClasses;
    }

    public void setFinalmethodsorclasses(boolean finalMethodsOrClasses) {
        this.finalMethodsOrClasses = finalMethodsOrClasses;
    }

    public List<String> getIncludes() {
        return includes;
    }

    public void setIncludes(String includes) {
        this.includes = RegularExpressionParser.parseRE(includes);
    }

    public Path createIncludeslist() {
        if (includesList == null) {
            includesList = new Path(getProject());
        }

        return includesList;
    }

    public Path getIncludeslist() {
        return includesList;
    }

    public List<String> getExcludes() {
        return excludes;
    }

    public void setExcludes(String excludes) {
        this.excludes = RegularExpressionParser.parseRE(excludes);
    }

    public Path createExcludeslist() {
        if (excludesList == null) {
            excludesList = new Path(getProject());
        }

        return excludesList;
    }

    public Path getExcludeslist() {
        return excludesList;
    }

    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 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 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;
    }

    // Visible for tests only
    void validateParameters() throws BuildException {
        if (getPath() == null) {
            throw new BuildException("path must be set!");
        }

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

    public void execute() throws BuildException {
        validateParameters();

        log("Reading classes from path " + getPath());

        VerboseListener verboseListener = new VerboseListener(this);

        SymbolGatherer gatherer = new SymbolGatherer(createStrategy());

        ClassfileLoader loader = new TransientClassfileLoader();
        loader.addLoadListener(new LoadListenerVisitorAdapter(gatherer));
        loader.addLoadListener(verboseListener);
        loader.load(Arrays.asList(getPath().list()));

        if (getCsv()) {
            printCSVFiles(gatherer);
        } else if (getJson()) {
            printJSONFile(gatherer);
        } else if (getText()) {
            printTextFile(gatherer);
        } else if (getXml()) {
            printXMLFile(gatherer);
        } else if (getYaml()) {
            printYAMLFile(gatherer);
        }
    }

    private void printCSVFiles(SymbolGatherer gatherer) throws BuildException {
        log("Saving symbols to " + getDestprefix().getAbsolutePath() + "_*.csv");

        try {
            new CSVSymbolPrinter(
                    null,
                    getClasses(),
                    getFields(),
                    getMethods(),
                    getLocalvariables(),
                    getInnerclasses(),
                    Optional.of(getDestprefix().getAbsolutePath())
            ).print(gatherer);
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    private void printJSONFile(SymbolGatherer gatherer) throws BuildException {
        String filename = getDestprefix().getAbsolutePath() + ".json";
        log("Saving symbols to " + filename);

        try (var out = new PrintWriter(new FileWriter(filename))) {
            new JSONSymbolPrinter(out).print(gatherer);
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    private void printTextFile(SymbolGatherer gatherer) throws BuildException {
        String filename = getDestprefix().getAbsolutePath() + ".txt";
        log("Saving symbols to " + filename);

        try (var out = new PrintWriter(new FileWriter(filename))) {
            new TextSymbolPrinter(out).print(gatherer);
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    private void printXMLFile(SymbolGatherer gatherer) throws BuildException {
        String filename = getDestprefix().getAbsolutePath() + ".xml";
        log("Saving symbols to " + filename);

        try (var out = new PrintWriter(new FileWriter(filename))) {
            new XMLSymbolPrinter(
                    out,
                    getEncoding(),
                    getDtdprefix(),
                    getIndenttext()
            ).print(gatherer);
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    private void printYAMLFile(SymbolGatherer gatherer) throws BuildException {
        String filename = getDestprefix().getAbsolutePath() + ".yaml";
        log("Saving symbols to " + filename);

        try (var out = new PrintWriter(new FileWriter(filename))) {
            new YAMLSymbolPrinter(
                    out,
                    getIndenttext()
            ).print(gatherer);
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    // Visible for testing only
    SymbolGathererStrategy createStrategy() {
        SymbolGathererStrategy result;

        if (getNonprivatefields()) {
            result = new NonPrivateFieldSymbolGathererStrategy();
        } else if (getFinalmethodsorclasses()) {
            result = new FinalMethodOrClassSymbolGathererStrategy();
        } else {
            result = createDefaultSymbolGathererStrategy();
        }

        result = new FilteringSymbolGathererStrategy(result, getIncludes(), loadCollection(getIncludeslist()), getExcludes(), loadCollection(getExcludeslist()));

        if (getPublicaccessibility() || getProtectedaccessibility() || getPrivateaccessibility() || getPackageaccessibility()) {
            result = new AccessibilitySymbolGathererStrategy(result, getPublicaccessibility(), getProtectedaccessibility(), getPrivateaccessibility(), getPackageaccessibility());
        }

        return result;
    }

    private SymbolGathererStrategy createDefaultSymbolGathererStrategy() {
        DefaultSymbolGathererStrategy result = new DefaultSymbolGathererStrategy();

        // Since DefaultSymbolGathererStrategy lists everything by default,
        // we turn them all off if any of the switches are present.
        // This way, if you pass nothing, you get the default behavior and
        // the tool shows everything.  If you pass in one or more, you only
        // see symbols of the kind(s) you specified.
        if (getClasses() || getFields() || getMethods() || getLocalvariables() || getInnerclasses()) {
            result.setMatchingClasses(false);
            result.setMatchingFields(false);
            result.setMatchingMethods(false);
            result.setMatchingLocalVariables(false);
            result.setMatchingInnerClasses(false);
        }

        if (getClasses()) {
            result.setMatchingClasses(true);
        }

        if (getFields()) {
            result.setMatchingFields(true);
        }

        if (getMethods()) {
            result.setMatchingMethods(true);
        }

        if (getLocalvariables()) {
            result.setMatchingLocalVariables(true);
        }

        if (getInnerclasses()) {
            result.setMatchingInnerClasses(true);
        }

        return result;
    }

    private Collection<String> loadCollection(Path path) {
        Collection<String> result = null;

        if (path != null) {
            result = Arrays.stream(path.list())
                    .map(Paths::get)
                    .flatMap(filepath -> {
                        try {
                            return Files.lines(filepath);
                        } catch (IOException ex) {
                            LogManager.getLogger(getClass()).error("Couldn't read file {}", filepath, ex);
                            return Stream.empty();
                        }
                    }).distinct()
                    .toList();
        }

        return result;
    }
}