io.bdeploy.common.cli.ToolBase Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of api Show documentation
Show all versions of api Show documentation
Public API including dependencies, ready to be used for integrations and plugins.
/*
* 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
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.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.
*/
@SuppressFBWarnings
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 {
ActivityReporter.Stream streamReporter = new ActivityReporter.Stream(System.out);
ActivityReporter defaultReporter = new ActivityReporter.Null();
ActivityReporter reporter = defaultReporter;
PrintStream output = null;
PrintStream reporterOutput = null;
DataFormat dataMode = DataFormat.TEXT;
RenderableResult result = null;
boolean verbose = false;
boolean closeOutput = false;
RuntimeException exc = null;
try {
int toolArgNum = 0;
for (int i = 0; i < args.length; ++i) {
if (args[i].startsWith("-")) {
switch (args[i]) {
case "-vv":
streamReporter.setVerboseSummary(true);
reporter = null;
verbose = true;
break;
case "-v":
verbose = true;
break;
case "-q":
// explicit new instance to signal explicit quiet-ness
reporter = new ActivityReporter.Null();
verbose = false;
break;
case "-o":
String of = args[++i];
closeOutput = true;
output = new PrintStream(new File(of), StandardCharsets.UTF_8.name());
break;
case "-op":
String opf = args[++i];
reporterOutput = new PrintStream(new File(opf), StandardCharsets.UTF_8.name());
streamReporter = new ActivityReporter.Stream(reporterOutput);
streamReporter.setVerboseSummary(reporter == null);
break;
case "--csv":
dataMode = DataFormat.CSV;
break;
case "--json":
dataMode = DataFormat.JSON;
break;
case "--version":
String version = VersionHelper.getVersion().toString();
System.out.println(version);
return;
default:
break;
}
} else {
toolArgNum = i;
break;
}
}
if (reporter == null) {
reporter = streamReporter;
}
if (output == null) {
output = System.out;
}
if (args.length <= toolArgNum || tools.get(args[toolArgNum]) == null) {
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. No effect if -q is given as well.");
System.out.println(LOGO[logo++] + " -o Write output to file . No effect on progress output.");
System.out.println(LOGO[logo++] + " -op Write progress tracking output to file . No");
System.out.println(LOGO[logo++] + " 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(LOGO[logo++]);
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("Tool", 20).column("Description", 60);
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());
}
}
CliTool instance = getTool(Arrays.copyOfRange(args, toolArgNum, args.length));
Class extends CliTool> clazz = instance.getClass();
ToolDefaultVerbose defVerbose = clazz.getAnnotation(ToolDefaultVerbose.class);
if (defVerbose != null) {
// switch to verbose, if we aren't already explicitly set up.
if (verbose || (verbose && reporter == streamReporter)) {
// explicit verbose -v || -vv
} else if (reporter != defaultReporter) {
// explicit quiet -q
} else {
// nothing explicit, respect annotation.
verbose = true;
if (defVerbose.value()) {
reporter = streamReporter;
}
}
}
if (reporter == streamReporter) {
streamReporter.beginReporting();
}
instance.setOutput(output);
instance.setVerbose(verbose);
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 (closeOutput && output != null) {
output.close();
}
streamReporter.stopReporting();
if (reporterOutput != null) {
reporterOutput.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 String getToolCategory(Class extends CliTool> 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 extends CliTool> 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 extends CliTool> 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 void register(Class extends CliTool> tool) {
namesOf(tool).forEach(name -> tools.put(name, tool));
}
/**
* Sets a factory for proper auditors to be used by the tool.
*/
public 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
*/
@SuppressFBWarnings
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 extends Annotation> 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("Argument", 20).column("Description", 70).column("Default", 10);
List> configsForHelp = getConfigsForHelp();
for (int i = 0; i < configsForHelp.size(); ++i) {
Class extends Annotation> 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