
io.bdeploy.common.cli.ToolBase Maven / Gradle / Ivy
/*
* 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 extends CliTool> 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 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 final 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 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 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(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 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 - 2025 Weber Informatics LLC | Privacy Policy