All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.bdeploy.common.cli.ToolBase Maven / Gradle / Ivy

Go to download

Public API including dependencies, ready to be used for integrations and plugins.

The newest version!
/*
 * Copyright (c) SSI Schaefer IT Solutions GmbH
 */
package io.bdeploy.common.cli;

import java.io.File;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.codahale.metrics.Timer;

import io.bdeploy.common.ActivityReporter;
import io.bdeploy.common.audit.Auditor;
import io.bdeploy.common.audit.NullAuditor;
import io.bdeploy.common.cfg.ConfigValidationException;
import io.bdeploy.common.cfg.Configuration;
import io.bdeploy.common.cfg.Configuration.Help;
import io.bdeploy.common.cli.data.DataFormat;
import io.bdeploy.common.cli.data.DataResult;
import io.bdeploy.common.cli.data.DataTable;
import io.bdeploy.common.cli.data.DataTableColumn;
import io.bdeploy.common.cli.data.ExitCode;
import io.bdeploy.common.cli.data.RenderableResult;
import io.bdeploy.common.metrics.Metrics;
import io.bdeploy.common.metrics.Metrics.MetricGroup;
import io.bdeploy.common.util.VersionHelper;

/**
 * Main CLI entry point base class.
 */
public abstract class ToolBase {

    private static final String[] LOGO = { //
            "┌──────────────────────────────┐  ", //
            "│                   ▄▄▄        │  ", //
            "│                 ██████       │  ", //
            "│   █▄           ███████▌      │  ", //
            "│     ▀█        ████████       │  ", //
            "│ ▀▀▀▀▄▄▀      ▐███████        │  ", //
            "│       ▀█▄▄▄  ▐████▀          │  ", //
            "│       ▄█████  ▀▀             │  ", //
            "│       ▀█████ ▐██  ██▌        │  ", //
            "│     ▐██████  ██▌ ▐██▌▐██     │  ", //
            "│      ████▀  ███ ▄███ ███▄    │  ", //
            "│                ▄███ ▄██▀     │  ", //
            "│                              │  ", //
            "└──────────────────────────────┘  ", //
    };

    private static boolean testMode = false;
    private static boolean testModeLLM = false;
    private static boolean failWithException = false;
    private final Map> tools = new TreeMap<>();
    private Function auditorFactory;

    /**
     * Indicate that the tools are executed in the context of a JUNIT test. In this mode the tools
     * will NOT take values from the environmental as fallback for command line arguments. Only arguments directly passed to the
     * tool are evaluated. Additionally the tools will fail with an exception without exiting the JVM.
     */
    public static void setTestMode(boolean test) {
        testMode = test;
        testModeLLM = test;
    }

    public static void setTestModeForLLM(boolean test) {
        testModeLLM = test;
    }

    /**
     * Indicates whether or not the tool should fail with an exception or should print out the errors to the command line.
     */
    public static void setFailWithException(boolean fail) {
        failWithException = fail;
    }

    /**
     * Returns whether or not the test mode has been enabled
     */
    public static boolean isTestMode() {
        return testMode;
    }

    public static boolean isTestModeLLM() {
        return testModeLLM;
    }

    public void toolMain(String... args) throws Exception {
        RenderableResult result = null;
        PrintStream output = null;
        PrintStream reporterOutput = null;
        ActivityReporter.Stream streamReporter = null;
        RuntimeException exc = null;

        boolean q = false;
        boolean v = false;
        boolean vv = false;
        String oArg = null;
        String opArg = null;
        DataFormat dataMode = DataFormat.TEXT;
        try {
            int toolArgNum = 0;
            for (int i = 0; i < args.length; ++i) {
                if (args[i].startsWith("-")) {
                    switch (args[i]) {
                        case "-q":
                            q = true;
                            v = false;
                            vv = false;
                            break;
                        case "-v":
                            q = false;
                            v = true;
                            vv = false;
                            break;
                        case "-vv":
                            q = false;
                            v = false;
                            vv = true;
                            break;
                        case "-o":
                            oArg = args[++i];
                            break;
                        case "-op":
                            opArg = args[++i];
                            break;
                        case "--csv":
                            dataMode = DataFormat.CSV;
                            break;
                        case "--json":
                            dataMode = DataFormat.JSON;
                            break;
                        case "--version":
                            System.out.println(VersionHelper.getVersion().toString());
                            return;
                        default:
                            break;
                    }
                } else {
                    toolArgNum = i;
                    break;
                }
            }

            output = oArg == null ? System.out : new PrintStream(new File(oArg), StandardCharsets.UTF_8.name());
            reporterOutput = opArg == null ? System.out : new PrintStream(new File(opArg), StandardCharsets.UTF_8.name());
            streamReporter = new ActivityReporter.Stream(reporterOutput);
            streamReporter.setVerboseSummary(vv);

            if (args.length <= toolArgNum || tools.get(args[toolArgNum]) == null) {
                printErrorAndExit();
                // this return statement is technically not required, because we never return from printErrorAndExit()
                return;
            }

            ActivityReporter reporter = vv ? streamReporter : new ActivityReporter.Null();
            CliTool instance = getTool(Arrays.copyOfRange(args, toolArgNum, args.length));
            Class clazz = instance.getClass();
            ToolDefaultVerbose defVerbose = clazz.getAnnotation(ToolDefaultVerbose.class);
            if (!q && !v && !vv && defVerbose != null) {
                // no explicit verbosity argument has been given AND we have a default verbosity -> set up according to the value
                if (defVerbose.value()) {
                    reporter = streamReporter;
                    vv = true;
                } else {
                    v = true;
                }
            }

            if (vv) {
                streamReporter.beginReporting();
            }

            instance.setOutput(output);
            instance.setVerbose(v || vv);
            instance.setActivityReporter(reporter);
            instance.setDataFormat(dataMode);

            if (instance instanceof ConfiguredCliTool) {
                if (((ConfiguredCliTool) instance).getRawConfiguration().getAllRawObjects().containsKey("help")) {
                    ((ConfiguredCliTool) instance).helpAndFail("Help:");
                }
            }

            try (Timer.Context timer = Metrics.getMetric(MetricGroup.CLI)
                    .timer(instance.getClass().getSimpleName() + "/" + args[toolArgNum]).time()) {
                result = instance.run();

                if (result != null) {
                    result.render();
                }
            }
        } catch (RuntimeException t) {
            exc = t;
        } finally {
            if (streamReporter != null) {
                streamReporter.stopReporting();
            }
            if (reporterOutput != null && reporterOutput != System.out) {
                reporterOutput.close();
            }
            if (output != null && output != System.out) {
                output.close();
            }
        }

        // don't system.exit in unit tests
        if (failWithException || testMode) {
            if (exc != null) {
                throw exc;
            } else {
                return; // normal return in tests.
            }
        }

        // explicit exit, otherwise non-daemon async jersey threads block.
        // The reason is not jersey itself, but it's usage of ForkJoinPool.commonPool.
        if (exc != null) {
            DataResult res = dataMode.createResult(output);
            res.setException(exc);
            res.render();

            System.exit(ExitCode.ERROR.getCode());
        }
        if (result == null) {
            System.exit(ExitCode.OK.getCode());
        } else {
            System.exit(result.getExitCode().getCode());
        }
    }

    private void printErrorAndExit() {
        int logo = 0;
        System.out.println(LOGO[logo++]);
        System.out.println(LOGO[logo++] + "BHive & BDeploy");
        System.out.println(LOGO[logo++] + "─────────────────────────────────────────────────");
        System.out.println(LOGO[logo++] + "Usage: $0   ");
        System.out.println(LOGO[logo++]);
        System.out.println(LOGO[logo++] + "Options:");
        System.out.println(LOGO[logo++] + "  -q      Be quiet - no progress reporting");
        System.out.println(LOGO[logo++] + "  -v|-vv  Be verbose | Be very verbose");
        System.out.println(LOGO[logo++] + "  -o   Write output to file  - no effect on");
        System.out.println(LOGO[logo++] + "          progress output");
        System.out.println(LOGO[logo++] + "  -op  Write progress tracking output to file");
        System.out.println(LOGO[logo++] + "           - no effect on normal output");
        System.out.println(LOGO[logo++] + "  --csv   Write data tables in CSV format");
        System.out.println(LOGO[logo++] + "  --json  Write data tables in JSON format");
        System.out.println();
        System.out.println("Tools:");
        System.out.println();
        Map>>> grouped = tools.entrySet().stream()
                .collect(Collectors.groupingBy(e -> getToolCategory(e.getValue()), TreeMap::new, Collectors.toList()));
        grouped.entrySet().stream().forEach(group -> {
            System.out.println("  " + group.getKey() + ":");

            DataTable table = DataFormat.TEXT.createTable(System.out);
            table.setIndentHint(5).setHideHeadersHint(true).setLineWrapHint(true);
            table.column(new DataTableColumn.Builder("Tool").setMinWidth(25).build());
            table.column(new DataTableColumn.Builder("Description").setMinWidth(30).build());

            group.getValue().stream().forEach(e -> {
                List names = namesOf(e.getValue());
                if (names.get(0).equals(e.getKey())) {
                    Help h = e.getValue().getAnnotation(Help.class);
                    table.row().cell(e.getKey()).cell(h.value() != null ? h.value() : "").build();
                } else {
                    table.row().cell(e.getKey()).cell("DEPRECATED - alias for '" + names.get(0) + "'").build();
                }
            });
            table.render();

            System.out.println();
        });

        if (failWithException || testMode) {
            throw new IllegalArgumentException("Wrong number of arguments");
        } else {
            System.exit(ExitCode.ERROR.getCode());
        }
    }

    private static String getToolCategory(Class clazz) {
        ToolCategory annotation = clazz.getAnnotation(ToolCategory.class);
        if (annotation == null || annotation.value() == null) {
            return "Ungrouped Tools";
        }
        return annotation.value();
    }

    /**
     * Retrieve the tool instance for the given command line. The tool is already
     * configured from command line arguments.
     */
    public CliTool getTool(String... args) throws Exception {
        Class tool = tools.get(args[0]);
        CliTool instance = tool.getDeclaredConstructor().newInstance();

        // make sure we pass on any auditor factory which is set.
        instance.setAuditorFactory(auditorFactory);

        if (instance instanceof ConfiguredCliTool) {
            Configuration cfg = new Configuration();
            if (args.length > 1) {
                cfg.add(Arrays.copyOfRange(args, 1, args.length));
            }
            ConfiguredCliTool toConfig = ((ConfiguredCliTool) instance);
            toConfig.setConfig(cfg);
        } else if (instance instanceof NativeCliTool) {
            ((NativeCliTool) instance).setArguments(Arrays.copyOfRange(args, 1, args.length));
        }
        return instance;
    }

    /**
     * @param tool the tool to inspect
     * @return all the names the tool should be known under. The 'official' name is the first in the list, the rest are aliases.
     */
    public static List namesOf(Class tool) {
        CliTool.CliName name = tool.getAnnotation(CliTool.CliName.class);
        if (name == null || name.value() == null) {
            throw new IllegalStateException("Cannot find annotation on " + tool);
        }

        List result = new ArrayList<>();
        result.add(name.value());

        if (name.alias().length > 0) {
            result.addAll(Arrays.asList(name.alias()));
        }
        return result;
    }

    public final void register(Class tool) {
        namesOf(tool).forEach(name -> tools.put(name, tool));
    }

    /**
     * Sets a factory for proper auditors to be used by the tool.
     */
    public final void setAuditorFactory(Function auditorFactory) {
        this.auditorFactory = auditorFactory;
    }

    /**
     * Base class for all CLI tools.
     */
    public abstract static class CliTool {

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        public @interface CliName {

            String value();

            String[] alias() default {};
        }

        private ActivityReporter reporter;
        private Function auditorFactory = p -> new NullAuditor();
        private PrintStream output = System.out;
        private boolean verbose;
        private DataFormat dataFormat = DataFormat.TEXT;

        /**
         * Set an alternative {@link ActivityReporter}.
         */
        public void setActivityReporter(ActivityReporter reporter) {
            this.reporter = reporter;
        }

        /**
         * @return the current {@link ActivityReporter}
         */
        protected ActivityReporter getActivityReporter() {
            return reporter;
        }

        public void setAuditorFactory(Function auditorFactory) {
            this.auditorFactory = auditorFactory == null ? (p -> new NullAuditor()) : auditorFactory;
        }

        protected Function getAuditorFactory() {
            return this.auditorFactory;
        }

        /**
         * Sets the mode to render tables with.
         */
        public void setDataFormat(DataFormat dataMode) {
            this.dataFormat = dataMode;
        }

        /**
         * @return the current mode tables are rendered in.
         */
        protected DataFormat getDataFormat() {
            return this.dataFormat;
        }

        protected DataTable createDataTable() {
            return dataFormat.createTable(output);
        }

        protected DataResult createSuccess() {
            return createResultWithSuccessMessage("Success");
        }

        protected DataResult createNoOp() {
            return createResultWithSuccessMessage("Nothing to do (missing arguments?)");
        }

        protected DataResult createEmptyResult() {
            return dataFormat.createResult(output);
        }

        protected DataResult createResultWithSuccessMessage(String message) {
            DataResult result = dataFormat.createResult(output);
            result.setMessage(message);
            result.setExitCode(ExitCode.OK);
            return result;
        }

        protected DataResult createResultWithErrorMessage(String message) {
            DataResult result = dataFormat.createResult(output);
            result.setMessage(message);
            result.setExitCode(ExitCode.ERROR);
            return result;
        }

        /**
         * Set an alternative output destination.
         */
        public void setOutput(PrintStream output) {
            this.output = output;
        }

        /**
         * Instructs tools which have some verbose output to print it.
         */
        public void setVerbose(boolean verbose) {
            this.verbose = verbose;
        }

        /**
         * @return whether verbose output should be produced
         */
        protected boolean isVerbose() {
            return verbose;
        }

        /**
         * @return the current output destination. Can be used for any output on the
         *         CLI.
         */
        protected PrintStream out() {
            return output;
        }

        /**
         * Execute the tool.
         */
        public abstract RenderableResult run();
    }

    /**
     * Base class for tools which require access to the actual command line they have been passed.
     */
    public abstract static class NativeCliTool extends CliTool {

        private String[] args;

        private void setArguments(String[] args) {
            this.args = args;
        }

        @Override
        public final RenderableResult run() {
            return run(args);
        }

        protected abstract RenderableResult run(String[] args);

    }

    /**
     * Base class for tools that accept additional configuration.
     *
     * @see Configuration
     */
    public abstract static class ConfiguredCliTool extends CliTool {

        private final Class configClass;
        private Configuration config;

        /**
         * @param configClass the configuration annotation class to use.
         */
        protected ConfiguredCliTool(Class configClass) {
            this.configClass = configClass;
        }

        /**
         * @param config the configuration proxy prepared by the framework
         */
        private void setConfig(Configuration config) {
            this.config = config;
        }

        /**
         * @return the type of the primary configuration annotation (the one passed in
         *         the constructor).
         */
        protected Class getPrimaryConfigClass() {
            return configClass;
        }

        /**
         * @return all {@link Annotation}s for which to render help output.
         */
        protected List> getConfigsForHelp() {
            return Collections.singletonList(configClass);
        }

        /**
         * Create a specific configuration from the underlying raw configuration using
         * the given annotation.
         */
        protected  X getConfig(Class clazz) {
            try {
                return config.get(clazz);
            } catch (ConfigValidationException e) {
                out().println("Validation Issues Exist:");
                for (Throwable t : e.getSuppressed()) {
                    out().println("  " + t.getMessage());
                }
                out().println();
                throw e;
            }
        }

        /**
         * @return the underlying raw configuration.
         */
        protected Configuration getRawConfiguration() {
            return config;
        }

        @Override
        public final RenderableResult run() {
            return run(getConfig(configClass));
        }

        /**
         * @param argument display help text and fail the tool if the argument is
         *            null.
         */
        protected void helpAndFailIfMissing(Object argument, String message) {
            if (argument == null || (argument.getClass().isArray() && Array.getLength(argument) == 0)
                    || ((argument instanceof String) && ((String) argument).isEmpty())) {
                helpAndFail("ERROR: " + message);
            }
        }

        /**
         * Display the help text and fail the tool (System.exit(1)).
         */
        protected void helpAndFail(String message) {
            out().println();
            out().println(message);
            out().println();
            DataTable table = getDataFormat().createTable(out());
            String name = getClass().getSimpleName();
            CliName annotation = getClass().getAnnotation(CliName.class);
            if (annotation != null && annotation.value() != null) {
                name = annotation.value();
            }
            Help help = getClass().getAnnotation(Help.class);
            table.setCaption(name + (help != null && help.value() != null ? (": " + help.value()) : ""));
            table.setLineWrapHint(true).setIndentHint(2);
            table.column(new DataTableColumn.Builder("Argument").setScaleToContent(true).build());
            table.column(new DataTableColumn.Builder("Description").setMinWidth(0).build());
            table.column(new DataTableColumn.Builder("Default").setMinWidth(7).build());

            List> configsForHelp = getConfigsForHelp();
            for (int i = 0; i < configsForHelp.size(); ++i) {
                Class x = configsForHelp.get(i);
                Configuration.formatHelp(x, table);
                if (i != (configsForHelp.size() - 1)) {
                    table.addHorizontalRuler();
                }
            }
            table.render();

            if (failWithException || testMode) {
                throw new IllegalArgumentException(message);
            } else {
                System.exit(ExitCode.ERROR.getCode());
            }
        }

        /**
         * Run the configured tool using configuration from the command line.
         *
         * @param config the configuration instance for the tool.
         */
        protected abstract RenderableResult run(T config);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy