Command.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.CollectingParameterStrategy;
import com.jeantessier.commandline.CommandLine;
import com.jeantessier.commandline.CommandLineException;
import com.jeantessier.commandline.CommandLineUsage;
import com.jeantessier.commandline.ParameterStrategy;
import com.jeantessier.commandline.Printer;
import com.jeantessier.commandline.TextPrinter;
import com.jeantessier.dependency.CollectionSelectionCriteria;
import com.jeantessier.dependency.ComprehensiveSelectionCriteria;
import com.jeantessier.dependency.NullSelectionCriteria;
import com.jeantessier.dependency.RegularExpressionSelectionCriteria;
import com.jeantessier.dependency.SelectionCriteria;
import com.jeantessier.dependencyfinder.Version;
import org.apache.logging.log4j.*;

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

public abstract class Command {
    public static final String DEFAULT_LOGFILE = "System.out";
    public static final String DEFAULT_INCLUDES = "//";

    private CommandLine commandLine;
    private CommandLineUsage commandLineUsage;

    private Date startTime;
    private VerboseListener verboseListener;
    private PrintWriter out;

    public String getName() {
        return getClass().getSimpleName();
    }

    private void resetCommandLine() {
        commandLine = new CommandLine(getParameterStrategy());
        populateCommandLineSwitches();
    }

    protected ParameterStrategy getParameterStrategy() {
        return new CollectingParameterStrategy();
    }

    protected CommandLine getCommandLine() {
        if (commandLine == null) {
            resetCommandLine();
        }

        return commandLine;
    }

    public CommandLineUsage getCommandLineUsage() {
        if (commandLineUsage == null) {
            commandLineUsage = new CommandLineUsage(getName());
            getCommandLine().accept(commandLineUsage);
        }

        return commandLineUsage;
    }

    protected VerboseListener getVerboseListener() {
        return verboseListener;
    }

    public void run(String[] args) throws Exception {
        if (validateCommandLine(args, System.err)) {
            process();
        } else {
            System.exit(1);
        }
    }

    protected void populateCommandLineSwitches() {
        getCommandLine().addToggleSwitch("echo");
        getCommandLine().addToggleSwitch("help");
        getCommandLine().addSingleValueSwitch("out");
        getCommandLine().addToggleSwitch("time");
        getCommandLine().addOptionalValueSwitch("verbose", DEFAULT_LOGFILE);
        getCommandLine().addToggleSwitch("version");
    }

    protected void populateCommandLineSwitchesForXMLOutput(String defaultEncoding, String defaultDTDPrefix, String defaultIndentText) {
        getCommandLine().addSingleValueSwitch("encoding", defaultEncoding);
        getCommandLine().addSingleValueSwitch("dtd-prefix", defaultDTDPrefix);
        getCommandLine().addSingleValueSwitch("indent-text", defaultIndentText);
    }

    protected Collection<CommandLineException> parseCommandLine(String[] args) {
        resetCommandLine();
        return getCommandLine().parse(args);
    }

    protected boolean validateCommandLine(String[] args, PrintStream out) {
        boolean result = true;

        Collection<CommandLineException> exceptions = parseCommandLine(args);

        if (getCommandLine().getToggleSwitch("version")) {
            showVersion(out);
            result = false;
        }

        if (getCommandLine().getToggleSwitch("help")) {
            showError(out);
            result = false;
        }

        if (getCommandLine().getToggleSwitch("echo")) {
            echo(out);
            result = false;
        }

        if (result) {
            exceptions.forEach(exception -> LogManager.getLogger(getClass()).error(exception));
            result = exceptions.isEmpty();
        }

        return result;
    }

    protected Collection<CommandLineException> validateCommandLineForScoping() {
        Collection<CommandLineException> exceptions = new ArrayList<>();

        if (hasScopeRegularExpressionSwitches() && hasScopeListSwitches()) {
            exceptions.add(new CommandLineException("You can use switches for regular expressions or lists for scope, but not at the same time"));
        }

        return exceptions;
    }

    protected Collection<CommandLineException> validateCommandLineForFiltering() {
        Collection<CommandLineException> exceptions = new ArrayList<>();

        if (hasFilterRegularExpressionSwitches() && hasFilterListSwitches()) {
            exceptions.add(new CommandLineException("You can use switches for regular expressions or lists for filter, but not at the same time"));
        }

        return exceptions;
    }

    private void process() throws Exception {
        startProcessing();
        doProcessing();
        stopProcessing();
    }

    private void startProcessing() throws IOException {
        startVerboseListener();
        // Output is started lazily the first time it is requested.
        startTimer();
    }

    protected abstract void doProcessing() throws Exception;

    private void stopProcessing() {
        stopTimer();
        stopOutput();
        stopVerboseListener();
    }

    private void startVerboseListener() throws IOException {
        verboseListener = new VerboseListener();
        if (commandLine.isPresent("verbose")) {
            if (DEFAULT_LOGFILE.equals(commandLine.getOptionalSwitch("verbose"))) {
                verboseListener.setWriter(new OutputStreamWriter(System.out));
            } else {
                verboseListener.setWriter(new FileWriter(commandLine.getOptionalSwitch("verbose")));
            }
        }
    }

    private void stopVerboseListener() {
        verboseListener.close();
    }

    private void startTimer() {
        startTime = new Date();
    }

    private void stopTimer() {
        if (commandLine.getToggleSwitch("time")) {
            Date end = new Date();
            System.err.println(getClass().getName() + ": " + ((end.getTime() - (double) startTime.getTime()) / 1000) + " secs.");
        }
    }

    private void startOutput() throws IOException {
        if (getCommandLine().isPresent("out")) {
            out = new PrintWriter(new FileWriter(getCommandLine().getSingleSwitch("out")));
        } else {
            out = new PrintWriter(new OutputStreamWriter(System.out));
        }
    }

    private void stopOutput() {
        if (out != null) {
            out.close();
        }
    }

    protected void echo() {
        echo(System.err);
    }

    protected void echo(PrintStream out) {
        Printer printer = new TextPrinter(getClass().getSimpleName());
        getCommandLine().accept(printer);
        out.println(printer);
    }

    protected void showError() {
        showError(System.err);
    }

    protected void showError(PrintStream out) {
        out.println(getCommandLineUsage());
        showSpecificUsage(out);
    }

    protected void showError(String msg) {
        showError(System.err, msg);
    }

    protected void showError(PrintStream out, String msg) {
        out.println(msg);
        showError(out);
    }

    protected abstract void showSpecificUsage(PrintStream out);

    protected void showVersion() {
        showVersion(System.err);
    }

    protected void showVersion(PrintStream out) {
        Version version = new Version();

        out.print(version.getImplementationTitle());
        out.print(" ");
        out.print(version.getImplementationVersion());
        out.print(" (c) ");
        out.print(version.getCopyrightDate());
        out.print(" ");
        out.print(version.getCopyrightHolder());
        out.println();

        out.print(version.getImplementationURL());
        out.println();

        out.print("Compiled on ");
        out.print(version.getImplementationDate());
        out.println();
    }

    protected void populateCommandLineSwitchesForScoping() {
        populateRegularExpressionCommandLineSwitches("scope", true, DEFAULT_INCLUDES);
        populateListCommandLineSwitches("scope");
    }

    protected void populateCommandLineSwitchesForFiltering() {
        populateRegularExpressionCommandLineSwitches("filter", true, DEFAULT_INCLUDES);
        populateListCommandLineSwitches("filter");
    }

    protected void populateCommandLineSwitchesForStartCondition() {
        populateRegularExpressionCommandLineSwitches("start", false, DEFAULT_INCLUDES);
        populateListCommandLineSwitches("start");
    }

    protected void populateCommandLineSwitchesForStopCondition() {
        populateRegularExpressionCommandLineSwitches("stop", false, null);
        populateListCommandLineSwitches("stop");
    }

    protected void populateRegularExpressionCommandLineSwitches(String name, boolean addToggles, String defaultIncludes) {
        if (defaultIncludes != null) {
            getCommandLine().addMultipleValuesSwitch(name + "-includes", defaultIncludes);
        } else {
            getCommandLine().addMultipleValuesSwitch(name + "-includes");
        }
        getCommandLine().addMultipleValuesSwitch(name + "-excludes");
        getCommandLine().addMultipleValuesSwitch("package-" + name + "-includes");
        getCommandLine().addMultipleValuesSwitch("package-" + name + "-excludes");
        getCommandLine().addMultipleValuesSwitch("class-" + name + "-includes");
        getCommandLine().addMultipleValuesSwitch("class-" + name + "-excludes");
        getCommandLine().addMultipleValuesSwitch("feature-" + name + "-includes");
        getCommandLine().addMultipleValuesSwitch("feature-" + name + "-excludes");

        if (addToggles) {
            getCommandLine().addToggleSwitch("package-" + name);
            getCommandLine().addToggleSwitch("class-" + name);
            getCommandLine().addToggleSwitch("feature-" + name);
        }
    }

    protected void populateListCommandLineSwitches(String name) {
        getCommandLine().addMultipleValuesSwitch(name + "-includes-list");
        getCommandLine().addMultipleValuesSwitch(name + "-excludes-list");
    }

    protected SelectionCriteria getScopeCriteria() {
        return getSelectionCriteria("scope", new ComprehensiveSelectionCriteria());
    }

    protected SelectionCriteria getFilterCriteria() {
        return getSelectionCriteria("filter", new ComprehensiveSelectionCriteria());
    }

    protected SelectionCriteria getStartCriteria() {
        return getSelectionCriteria("start", new ComprehensiveSelectionCriteria());
    }

    protected SelectionCriteria getStopCriteria() {
        return getSelectionCriteria("stop", new NullSelectionCriteria());
    }

    protected SelectionCriteria getSelectionCriteria(String name, SelectionCriteria defaultSelectionCriteria) {
        SelectionCriteria result = defaultSelectionCriteria;

        if (hasRegularExpressionSwitches(name)) {
            RegularExpressionSelectionCriteria regularExpressionFilterCriteria = new RegularExpressionSelectionCriteria();

            if (getCommandLine().isPresent("package-" + name) || getCommandLine().isPresent("class-" + name) || getCommandLine().isPresent("feature-" + name)) {
                regularExpressionFilterCriteria.setMatchingPackages(getCommandLine().getToggleSwitch("package-" + name));
                regularExpressionFilterCriteria.setMatchingClasses(getCommandLine().getToggleSwitch("class-" + name));
                regularExpressionFilterCriteria.setMatchingFeatures(getCommandLine().getToggleSwitch("feature-" + name));
            }

            if (getCommandLine().isPresent(name + "-includes") || (!getCommandLine().isPresent("package-" + name + "-includes") && !getCommandLine().isPresent("class-" + name + "-includes") && !getCommandLine().isPresent("feature-" + name + "-includes"))) {
                // Only use the default if nothing else has been specified.
                regularExpressionFilterCriteria.setGlobalIncludes(getCommandLine().getMultipleSwitch(name + "-includes"));
            }
            regularExpressionFilterCriteria.setGlobalExcludes(getCommandLine().getMultipleSwitch(name + "-excludes"));
            regularExpressionFilterCriteria.setPackageIncludes(getCommandLine().getMultipleSwitch("package-" + name + "-includes"));
            regularExpressionFilterCriteria.setPackageExcludes(getCommandLine().getMultipleSwitch("package-" + name + "-excludes"));
            regularExpressionFilterCriteria.setClassIncludes(getCommandLine().getMultipleSwitch("class-" + name + "-includes"));
            regularExpressionFilterCriteria.setClassExcludes(getCommandLine().getMultipleSwitch("class-" + name + "-excludes"));
            regularExpressionFilterCriteria.setFeatureIncludes(getCommandLine().getMultipleSwitch("feature-" + name + "-includes"));
            regularExpressionFilterCriteria.setFeatureExcludes(getCommandLine().getMultipleSwitch("feature-" + name + "-excludes"));

            result = regularExpressionFilterCriteria;
        } else if (hasListSwitches(name)) {
            result = createCollectionSelectionCriteria(getCommandLine().getMultipleSwitch(name + "-includes-list"), getCommandLine().getMultipleSwitch(name + "-excludes-list"));
        }
        
        return result;
    }

    protected boolean hasScopeRegularExpressionSwitches() {
        return hasRegularExpressionSwitches("scope");
    }

    protected boolean hasFilterRegularExpressionSwitches() {
        return hasRegularExpressionSwitches("filter");
    }

    protected boolean hasRegularExpressionSwitches(String name) {
        Collection<String> switches = getCommandLine().getPresentSwitches();

        return
            switches.contains(name + "-includes") ||
            switches.contains(name + "-excludes") ||
            switches.contains("package-" + name) ||
            switches.contains("package-" + name + "-includes") ||
            switches.contains("package-" + name + "-excludes") ||
            switches.contains("class-" + name) ||
            switches.contains("class-" + name + "-includes") ||
            switches.contains("class-" + name + "-excludes") ||
            switches.contains("feature-" + name) ||
            switches.contains("feature-" + name + "-includes") ||
            switches.contains("feature-" + name + "-excludes");
    }

    protected boolean hasScopeListSwitches() {
        return hasListSwitches("scope");
    }

    protected boolean hasFilterListSwitches() {
        return hasListSwitches("filter");
    }

    protected boolean hasListSwitches(String name) {
        Collection<String> switches = getCommandLine().getPresentSwitches();

        return
            switches.contains(name + "-includes-list") ||
            switches.contains(name + "-excludes-list");
    }

    protected CollectionSelectionCriteria createCollectionSelectionCriteria(Collection<String> includes, Collection<String> excludes) {
        return new CollectionSelectionCriteria(loadCollection(includes), loadCollection(excludes));
    }

    protected Collection<String> loadCollection(Collection<String> filenames) {
        return filenames.stream()
                .map(Paths::get)
                .flatMap(path -> {
                    try {
                        return Files.lines(path);
                    } catch (IOException ex) {
                        LogManager.getLogger(getClass()).error("Couldn't read file {}", path, ex);
                        return Stream.empty();
                    }
                }).distinct()
                .toList();
    }

    protected PrintWriter getOut() throws IOException {
        if (out == null) {
            startOutput();
        }

        return out;
    }

    protected void setOut(PrintWriter out) {
        this.out = out;
    }
}