From efb377ac6f29ecbb927184b023e775694780429d Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:47:38 +0200 Subject: [PATCH] chore: Partial implementation --- .../app/runner/DefaultFortifyCLIRunner.java | 2 +- .../util/FortifyCLIDynamicInitializer.java | 1 + .../cmd/AbstractActionAsciidocCommand.java | 4 +- .../cli/cmd/AbstractActionHelpCommand.java | 4 +- .../cli/cmd/AbstractActionRunCommand.java | 4 +- .../helper/ActionCommandLineFactory.java | 133 +++++++++++++ .../action/helper/ActionParameterHelper.java | 181 ++++++++++++++++++ .../common/action/runner/ActionRunner.java | 8 +- .../action/runner/ActionRunnerCommand.java | 39 ++++ ...per.java => OldActionParameterHelper.java} | 36 +++- .../util/FortifyCLIDefaultValueProvider.java | 3 +- .../cli/common/i18n/ActionMessages.properties | 18 ++ .../cli/fod/action/cli/cmd/ActionCommand.java | 89 --------- .../cli/fod/action/cli/cmd/Test2Command.java | 16 +- .../cli/fod/action/cli/cmd/TestCommand.java | 11 +- .../fod/action/helper/FoDActionHelper.java | 56 ++++++ 16 files changed, 493 insertions(+), 112 deletions(-) create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionCommandLineFactory.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunnerCommand.java rename fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/{ActionParameterHelper.java => OldActionParameterHelper.java} (66%) rename fcli-core/{fcli-app/src/main/java/com/fortify/cli/app/runner => fcli-common/src/main/java/com/fortify/cli/common/cli}/util/FortifyCLIDefaultValueProvider.java (97%) create mode 100644 fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/ActionMessages.properties delete mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/ActionCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/helper/FoDActionHelper.java diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/DefaultFortifyCLIRunner.java b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/DefaultFortifyCLIRunner.java index 2cc2d80865..79c515457c 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/DefaultFortifyCLIRunner.java +++ b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/DefaultFortifyCLIRunner.java @@ -16,9 +16,9 @@ import java.util.List; import com.fortify.cli.app._main.cli.cmd.FCLIRootCommands; -import com.fortify.cli.app.runner.util.FortifyCLIDefaultValueProvider; import com.fortify.cli.app.runner.util.FortifyCLIDynamicInitializer; import com.fortify.cli.app.runner.util.FortifyCLIStaticInitializer; +import com.fortify.cli.common.cli.util.FortifyCLIDefaultValueProvider; import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; import com.fortify.cli.common.variable.FcliVariableHelper; diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java index 3b9eae2000..c8f9a33da3 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java +++ b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java @@ -22,6 +22,7 @@ import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand.GenericOptionsArgGroup; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand.LogLevel; +import com.fortify.cli.common.cli.util.FortifyCLIDefaultValueProvider; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java index cffb960ee2..8dfeef492d 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java @@ -26,7 +26,7 @@ import com.fortify.cli.common.action.helper.ActionLoaderHelper; import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; import com.fortify.cli.common.action.model.Action; -import com.fortify.cli.common.action.runner.ActionParameterHelper; +import com.fortify.cli.common.action.runner.OldActionParameterHelper; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.cli.mixin.CommonOptionMixins; import com.fortify.cli.common.cli.util.SimpleOptionsParser.IOptionDescriptor; @@ -104,7 +104,7 @@ private final String generateActionSection(Action action) { } private final String generateOptionsSection(Action action) { - return ActionParameterHelper.getOptionDescriptors(action) + return OldActionParameterHelper.getOptionDescriptors(action) .stream().map(this::generateOptionDescription).collect(Collectors.joining("\n\n")); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java index 4f10a26f60..4ac961c083 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.action.cli.mixin.ActionResolverMixin; import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; import com.fortify.cli.common.action.model.Action; -import com.fortify.cli.common.action.runner.ActionParameterHelper; +import com.fortify.cli.common.action.runner.OldActionParameterHelper; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureMetadata; @@ -54,7 +54,7 @@ private final String getActionHelp(Action action) { "%s"+ "\nAction options:\n"+ "%s", - metadata.getName(), usage.getHeader(), usage.getDescription(), getMetadata(action), ActionParameterHelper.getSupportedOptionsTable(action)); + metadata.getName(), usage.getHeader(), usage.getDescription(), getMetadata(action), OldActionParameterHelper.getSupportedOptionsTable(action)); } private final String getMetadata(Action action) { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java index 652530708e..99bfc2ffec 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.action.cli.mixin.ActionResolverMixin; import com.fortify.cli.common.action.cli.mixin.ActionValidationMixin; -import com.fortify.cli.common.action.runner.ActionParameterHelper; +import com.fortify.cli.common.action.runner.OldActionParameterHelper; import com.fortify.cli.common.action.runner.ActionRunner; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.cli.mixin.CommandHelperMixin; @@ -93,7 +93,7 @@ private void setOrClearSystemProperty(String name, String value) { private ParameterException onValidationErrors(OptionsParseResult optionsParseResult) { var errorsString = String.join("\n ", optionsParseResult.getValidationErrors()); - var supportedOptionsString = ActionParameterHelper.getSupportedOptionsTable(optionsParseResult.getOptions()); + var supportedOptionsString = OldActionParameterHelper.getSupportedOptionsTable(optionsParseResult.getOptions()); var msg = String.format("Option errors:\n %s\nSupported options:\n%s\n", errorsString, supportedOptionsString); return new ParameterException(commandHelper.getCommandSpec().commandLine(), msg); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionCommandLineFactory.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionCommandLineFactory.java new file mode 100644 index 0000000000..3c80512b50 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionCommandLineFactory.java @@ -0,0 +1,133 @@ +/** + * Copyright 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.common.action.helper; + +import java.util.concurrent.Callable; +import java.util.stream.StreamSupport; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.runner.ActionRunnerCommand; +import com.fortify.cli.common.cli.util.FortifyCLIDefaultValueProvider; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureMetadata; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.util.StringUtils; + +import lombok.Builder; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; + +@Builder +public class ActionCommandLineFactory { + /** Action run command for which to generate a CommandLine instance */ + private final String runCmd; + /** Action to run, provided through builder method */ + private final Action action; + /** ActionParameterHelper instance, configured through builder method */ + private final ActionParameterHelper actionParameterHelper; + /** ActionRunnerCommand instance, configured through builder method */ + private final ActionRunnerCommand actionRunnerCommand; + + public final CommandLine createCommandLine() { + CommandLine cl = new CommandLine(createCommandSpec()); + cl.setDefaultValueProvider(FortifyCLIDefaultValueProvider.getInstance()); + return cl; + } + + private final CommandSpec createCommandSpec() { + CommandSpec newRunCmd = createRunSpec(); + CommandSpec actionCmd = CommandSpec.forAnnotatedObject(actionRunnerCommand); + addUsage(actionCmd); + actionParameterHelper.addOptions(actionCmd); + newRunCmd.addSubcommand(action.getMetadata().getName(), actionCmd); + return actionCmd; + } + + private final void addUsage(CommandSpec actionCmd) { + actionCmd.usageMessage().header(action.getUsage().getHeader()); + actionCmd.usageMessage().description(getDescription()); + } + + private final String getDescription() { + // TODO Add signature metadata from action.getMetadata() + // TODO Improve formatting? Or just have yaml files provide string? + return action.getUsage().getDescription().trim()+"\n\n"+getMetadataDescription().trim(); + } + + private final String getMetadataDescription() { + var metadata = action.getMetadata(); + var signatureDescriptor = metadata.getSignatureDescriptor(); + var signatureMetadata = signatureDescriptor==null ? null : signatureDescriptor.getMetadata(); + if ( signatureMetadata==null ) { signatureMetadata = SignatureMetadata.builder().build(); } + var extraSignatureInfo = signatureMetadata.getExtraInfo(); + var publicKeyDescriptor = metadata.getPublicKeyDescriptor(); + if ( publicKeyDescriptor==null ) { publicKeyDescriptor = PublicKeyDescriptor.builder().build(); } + var signatureStatus = metadata.getSignatureStatus(); + var data = JsonHelper.getObjectMapper().createObjectNode(); + data.put("Origin", metadata.isCustom()?"CUSTOM":"FCLI"); + data.put("Signature status", signatureStatus.toString()); + data.put("Author", StringUtils.ifBlank(action.getAuthor(), "N/A")); + if ( signatureStatus!=SignatureStatus.UNSIGNED ) { + data.put("Signed by", StringUtils.ifBlank(signatureMetadata.getSigner(), "N/A")); + } + switch (signatureStatus) { + case NO_PUBLIC_KEY: + data.put("Required public key", StringUtils.ifBlank(signatureDescriptor.getPublicKeyFingerprint(), "N/A")); + break; + case VALID: + data.put("Certified by", StringUtils.ifBlank(publicKeyDescriptor.getName(), + StringUtils.ifBlank(publicKeyDescriptor.getFingerprint(), "N/A"))); + break; + default: break; + } + if ( extraSignatureInfo!=null && extraSignatureInfo.size()>0 ) { + data.set("Extra signature info", extraSignatureInfo); + } + return "Metadata:\n"+toString(data, " "); + } + + private static final String toString(ObjectNode data, String indent) { + var sb = new StringBuffer(); + Iterable iterable = () -> data.fieldNames(); + var nameLength = StreamSupport.stream(iterable.spliterator(), false) + .mapToInt(String::length) + .max().getAsInt(); + var fmt = indent+"%-"+(nameLength+1)+"s %s\n"; + data.fields().forEachRemaining(e->sb.append(String.format(fmt, e.getKey()+":", toValue(e.getValue(), indent)))); + return sb.toString(); + } + + private static final String toValue(JsonNode value, String originalIndent) { + if ( value instanceof ObjectNode ) { + return "\n"+toString((ObjectNode)value, originalIndent+" "); + } else { + return value.asText(); + } + } + + private final CommandSpec createRunSpec() { + return CommandSpec.create().name(runCmd).resourceBundleBaseName("com.fortify.cli.common.i18n.ActionMessages"); + } + + @Command + private static final class DummyCommand implements Callable { + @Override + public Integer call() throws Exception { + return 0; + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java new file mode 100644 index 0000000000..cc28bc698c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java @@ -0,0 +1,181 @@ +/** + * Copyright 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.common.action.helper; + +import java.util.ArrayList; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.model.ActionParameter; +import com.fortify.cli.common.action.runner.ActionRunner.ParameterTypeConverterArgs; +import com.fortify.cli.common.action.runner.ActionSpelFunctions; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.spring.expression.IConfigurableSpelEvaluator; +import com.fortify.cli.common.spring.expression.SpelEvaluator; +import com.fortify.cli.common.spring.expression.SpelHelper; +import com.fortify.cli.common.util.StringUtils; + +import lombok.Builder; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Singular; +import picocli.CommandLine.ITypeConverter; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.OptionSpec; + +@Builder +public final class ActionParameterHelper { + /** Logger */ + private static final Logger LOG = LoggerFactory.getLogger(ActionParameterHelper.class); + /** Static SpEL evaluator configured with {@link ActionSpelFunctions} */ + private static final IConfigurableSpelEvaluator spelEvaluator = SpelEvaluator.JSON_GENERIC.copy() + .configure(c->SpelHelper.registerFunctions(c, ActionSpelFunctions.class)); + + /** Action instance, configured through builder method */ + @NonNull private final Action action; + /** Type-based OptionSpec configurers */ + @Singular private final Map> optionSpecTypeConfigurers; + /** What action to take on unknown parameter types, configured through builder method */ + private final OnUnknownParameterType onUnknownParameterType; + + public final void addOptions(CommandSpec actionCmd) { + for ( var p : action.getParameters() ) { + actionCmd.addOption(createOptionSpec(p)); + } + } + + public final ObjectNode getParameterValues(CommandSpec actionCmd, Function parameterTypeConverterArgsSupplier) { + ObjectNode result = JsonHelper.getObjectMapper().createObjectNode(); + for ( var p : action.getParameters() ) { + var name = p.getName(); + var option = actionCmd.findOption(name); + if ( option==null ) { throw new IllegalStateException("Can't find option for parameter name: "+name); } + var value = option.getValue(); + if ( value instanceof ParameterValueSupplier ) { + value = ((ParameterValueSupplier)value).getValue(parameterTypeConverterArgsSupplier.apply(p)); + } + result.putPOJO(name, value); + } + return result; + } + + private final OptionSpec createOptionSpec(ActionParameter p) { + var builder = OptionSpec.builder(getOptionNames(p)); + configureType(builder, p); + if ( p.getDefaultValue()!=null ) { + builder.defaultValue(spelEvaluator.evaluate(p.getDefaultValue(), null, String.class)); + } + builder.description(p.getDescription()); + builder.required(p.isRequired()); + return builder.build(); + } + + private void configureType(OptionSpec.Builder builder, ActionParameter p) { + var type = p.getType(); + var configurer = optionSpecTypeConfigurers.get(StringUtils.ifBlank(type, "string")); + if ( configurer==null ) { + (onUnknownParameterType==null ? OnUnknownParameterType.ERROR : onUnknownParameterType).configure(builder, p); + } else { + configurer.accept(builder, p); + } + } + + private static final String[] getOptionNames(ActionParameter p) { + var names = new ArrayList(); + names.add(getOptionName(p.getName())); + for ( var alias : p.getCliAliasesArray() ) { + names.add(getOptionName(alias)); + } + return names.toArray(String[]::new); + } + + private static final String getOptionName(String name) { + var prefix = name.length()==1 ? "-" : "--"; + return prefix+name; + } + + @RequiredArgsConstructor + public static enum OnUnknownParameterType { + WARN(OnUnknownParameterType::warn), ERROR(OnUnknownParameterType::error); + + private final BiConsumer configurer; + + public void configure(OptionSpec.Builder builder, ActionParameter p) { + configurer.accept(builder, p); + } + + private static final void warn(OptionSpec.Builder builder, ActionParameter p) { + LOG.warn("WARN: "+getMessage(p)+", action will fail to run"); + builder.arity("1").type(String.class).paramLabel(""); + } + + private static final void error(OptionSpec.Builder builder, ActionParameter p) { + throw new IllegalStateException(getMessage(p)); + } + + private static final String getMessage(ActionParameter p) { + return "Unknow parameter type '"+p.getType()+"' for parameter '"+p.getName()+"'"; + } + } + + public static class ActionParameterHelperBuilder { + public ActionParameterHelperBuilder() { + addDefaultOptionSpecTypeConfigurers(); + } + + private final void addDefaultOptionSpecTypeConfigurers() { + optionSpecTypeConfigurer("string", (b,p)->b.arity("1").type(String.class)); + optionSpecTypeConfigurer("boolean", (b,p)->b.arity("0..1").type(Boolean.class).defaultValue("false")); + optionSpecTypeConfigurer("int", (b,p)->b.arity("1").type(Integer.class)); + optionSpecTypeConfigurer("long", (b,p)->b.arity("1").type(Long.class)); + optionSpecTypeConfigurer("double", (b,p)->b.arity("1").type(Double.class)); + optionSpecTypeConfigurer("float", (b,p)->b.arity("1").type(Float.class)); + optionSpecTypeConfigurer("array", (b,p)->b.arity("1").type(ArrayNode.class).converters(new ArrayNodeConverter())); + } + } + + private static final class ArrayNodeConverter implements ITypeConverter { + @Override + public ArrayNode convert(String value) throws Exception { + return StringUtils.isBlank(value) + ? JsonHelper.toArrayNode(new String[] {}) + : JsonHelper.toArrayNode(value.split(",")); + } + } + + // TODO What values should we store in this class (like ActionParameter object), + // and what values should be passed on the getValue method? + @RequiredArgsConstructor + public static final class ParameterValueSupplier { + private final String value; + private final BiFunction converter; + + public JsonNode getValue(ParameterTypeConverterArgs args) { + return converter.apply(value, args); + } + + public static final OptionSpec.Builder configure(OptionSpec.Builder builder, BiFunction converter) { + builder.arity("1").type(ParameterValueSupplier.class).converters(value->new ParameterValueSupplier(value, converter)); + return builder; + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunner.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunner.java index 30eee78e74..4d9acd93a0 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunner.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunner.java @@ -284,7 +284,7 @@ private final OptionsParseResult processParameters() { } private final OptionsParseResult parseParameterValues(String[] args) { - List optionDescriptors = ActionParameterHelper.getOptionDescriptors(action); + List optionDescriptors = OldActionParameterHelper.getOptionDescriptors(action); var parseResult = new SimpleOptionsParser(optionDescriptors).parse(args); addDefaultValues(parseResult); addValidationMessages(parseResult); @@ -308,13 +308,13 @@ private final void addDefaultValue(OptionsParseResult parseResult, ActionParamet ? null : spelEvaluator.evaluate(defaultValueExpression, globalData, String.class); } - parseResult.getOptionValuesByName().put(ActionParameterHelper.getOptionName(name), value); + parseResult.getOptionValuesByName().put(OldActionParameterHelper.getOptionName(name), value); } private final void addValidationMessages(OptionsParseResult parseResult, ActionParameter parameter) { if ( parameter.isRequired() && StringUtils.isBlank(getOptionValue(parseResult, parameter)) ) { parseResult.getValidationErrors().add("No value provided for required option "+ - ActionParameterHelper.getOptionName(parameter.getName())); + OldActionParameterHelper.getOptionName(parameter.getName())); } } @@ -330,7 +330,7 @@ private final void addParameterData(ActionParameter parameter) { parameters.set(name, convertParameterValue(value, parameter)); } private String getOptionValue(OptionsParseResult parseResult, ActionParameter parameter) { - var optionName = ActionParameterHelper.getOptionName(parameter.getName()); + var optionName = OldActionParameterHelper.getOptionName(parameter.getName()); return parseResult.getOptionValuesByName().get(optionName); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunnerCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunnerCommand.java new file mode 100644 index 0000000000..cb25cc418d --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunnerCommand.java @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.common.action.runner; + +import java.util.concurrent.Callable; + +import com.fortify.cli.common.action.helper.ActionParameterHelper; +import com.fortify.cli.common.action.model.Action; + +import lombok.Builder; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Spec; + +@Command +@Builder +public class ActionRunnerCommand implements Callable { + /** Action to run, provided through builder method */ + private final Action action; + /** ActionParameterHelper instance, configured through builder method */ + private final ActionParameterHelper actionParameterHelper; + @Spec private CommandSpec spec; + + public Integer call() { + System.out.println(spec); + System.out.println(actionParameterHelper.getParameterValues(spec, null)); + return 0; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/OldActionParameterHelper.java similarity index 66% rename from fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/OldActionParameterHelper.java index e9ea65a9d6..f1cf231907 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/OldActionParameterHelper.java @@ -24,9 +24,39 @@ import com.github.freva.asciitable.Column; import com.github.freva.asciitable.HorizontalAlign; -public final class ActionParameterHelper { - private ActionParameterHelper() {} +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.OptionSpec; + +public final class OldActionParameterHelper { + private OldActionParameterHelper() {} + + public static final void addOptions(CommandSpec spec, Action action) { + for ( var p : action.getParameters() ) { + spec.addOption(createOptionSpec(p)); + } + } + private static final OptionSpec createOptionSpec(ActionParameter p) { + return OptionSpec.builder(getOptionName(p.getName()), getOptionAliases(p.getCliAliasesArray()).toArray(String[]::new)) + .arity("boolean".equals(p.getType()) ? "0..1" : "1") + //.auxiliaryTypes(String.class) + .type(getType(p)) + // TODO Evaluate SpEL expression + .defaultValue(p.getDefaultValue()==null?null:p.getDefaultValue().getExpressionString()) + .description(p.getDescription()) + .required(p.isRequired()) + .build(); + } + + private static Class getType(ActionParameter p) { + // TODO Support other primitive types like numbers. + if ( "boolean".equals(p.getType()) ) { + return Boolean.class; + } else { + return String.class; + } + } + public static final List getOptionDescriptors(Action action) { var parameters = action.getParameters(); List result = new ArrayList<>(parameters.size()); @@ -48,7 +78,7 @@ static final String getOptionName(String parameterNameOrAlias) { } private static final List getOptionAliases(String[] aliases) { - return aliases==null ? null : Stream.of(aliases).map(ActionParameterHelper::getOptionName).toList(); + return aliases==null ? null : Stream.of(aliases).map(OldActionParameterHelper::getOptionName).toList(); } public static final String getSupportedOptionsTable(Action action) { diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDefaultValueProvider.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/FortifyCLIDefaultValueProvider.java similarity index 97% rename from fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDefaultValueProvider.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/FortifyCLIDefaultValueProvider.java index f5b916a278..2d9a83a093 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDefaultValueProvider.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/FortifyCLIDefaultValueProvider.java @@ -10,11 +10,10 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.app.runner.util; +package com.fortify.cli.common.cli.util; import java.lang.reflect.AnnotatedElement; -import com.fortify.cli.common.cli.util.EnvSuffix; import com.fortify.cli.common.util.EnvHelper; import com.fortify.cli.common.util.StringUtils; diff --git a/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/ActionMessages.properties b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/ActionMessages.properties new file mode 100644 index 0000000000..0dcf8d359d --- /dev/null +++ b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/ActionMessages.properties @@ -0,0 +1,18 @@ +# Error messages: +error.missing.subcommand = Missing required subcommand +error.missing.parameter = Missing required parameter: +error.missing.option = Missing required option +error.missing.confirmation = Interactive console not available; use -y / --confirm option to confirm:\n %s\n +error.unmatched.argument = Unmatched argument at index +error.unmatched.command = Did you mean + +# Generic help text elements +usage.synopsisHeading = %nUsage:\u0020 +usage.descriptionHeading = %n +usage.footer.0 = %nCommands/options marked as PREVIEW are subject to change; pipelines or scripts using these \ + may need to be updated on any fcli upgrade. +usage.footer.1 = %nFull command list: fcli util all-commands list +usage.footer.2 = Documentation: https://fortify.github.io/fcli +usage.footer.3 = %n(c) Copyright 2021-2024 Open Text +usage.parameterListHeading = %nCommand parameters%n +usage.optionListHeading = %nCommand options%n \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/ActionCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/ActionCommand.java deleted file mode 100644 index d747bbaca5..0000000000 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/ActionCommand.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright 2023 Open Text. - * - * The only warranties for products and services of Open Text - * and its affiliates and licensors ("Open Text") are as may - * be set forth in the express warranty statements accompanying - * such products and services. Nothing herein should be construed - * as constituting an additional warranty. Open Text shall not be - * liable for technical or editorial errors or omissions contained - * herein. The information contained herein is subject to change - * without notice. - */ -package com.fortify.cli.fod.action.cli.cmd; - -import java.util.List; -import java.util.concurrent.Callable; - -import com.fortify.cli.common.action.model.Action; -import com.fortify.cli.common.action.model.Action.ActionMetadata; -import com.fortify.cli.common.action.model.ActionParameter; -import com.fortify.cli.common.action.runner.ActionParameterHelper; - -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Model.OptionSpec; -import picocli.CommandLine.Spec; - -@Command -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class ActionCommand implements Callable { - private final Action action; - @Spec - private CommandSpec spec; - - private static final CommandSpec asCommandSpec( - CommandSpec runCmd, // Represents applicable 'action run' command like 'fcli fod action run' - Action action // Action instance to be run - ) { - CommandSpec root = CommandSpec.create().resourceBundleBaseName("com.fortify.cli.common.i18n.FortifyCLIMessages"); - CommandSpec newRunCmd = replicateRunCmd(root, runCmd); - CommandSpec actionCmd = CommandSpec.forAnnotatedObject(new ActionCommand(action)); - addUsage(actionCmd, action); - addOptions(actionCmd, action); - newRunCmd.addSubcommand(action.getMetadata().getName(), actionCmd); - return actionCmd; - } - - private static void addUsage(CommandSpec actionCmd, Action action) { - actionCmd.usageMessage().header(action.getUsage().getHeader()); - actionCmd.usageMessage().description(action.getUsage().getDescription()); - - } - - private static void addOptions(CommandSpec actionCmd, Action action) { - for ( var o : ActionParameterHelper.getOptionDescriptors(action) ) { - var optionSpec = OptionSpec.builder(o.getName(), o.getAliases().toArray(String[]::new)) - .arity("1") // TODO arity 0..1 for boolean options, ... - .description(o.getDescription()) - .build(); - actionCmd.addOption(optionSpec); - } - } - - private static CommandSpec replicateRunCmd(CommandSpec root, CommandSpec runCmd) { - // TODO Iterate over runCmd and parents to replicate command structure - var result = CommandSpec.create().name("run"); - result.parent(CommandSpec.create().name("action")); - return result; - } - - - public static final CommandLine asCommandLine( - CommandSpec runCmd, // Represents applicable 'action run' command like 'fcli fod action run' - Action action // Action instance to be run - ) { - CommandLine cl = new CommandLine(asCommandSpec(runCmd, action)); - //cl.setDefaultValueProvider(FortifyCLIDefaultValueProvider.getInstance()); - return cl; - } - - public Integer call() { - System.out.println(spec); - spec.options().forEach(o->System.out.println(String.format("%s: %s", o.longestName(), o.getValue()))); - return 0; - } -} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/Test2Command.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/Test2Command.java index 4bc47b11ba..13f18d4885 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/Test2Command.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/Test2Command.java @@ -14,25 +14,33 @@ import com.fortify.cli.common.action.cli.mixin.ActionResolverMixin; import com.fortify.cli.common.action.cli.mixin.ActionValidationMixin; +import com.fortify.cli.common.action.helper.ActionParameterHelper.OnUnknownParameterType; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.cli.mixin.CommandHelperMixin; +import com.fortify.cli.fod.action.helper.FoDActionHelper; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; +import picocli.CommandLine.Unmatched; @Command(name = "test2") public class Test2Command extends AbstractRunnableCommand { @Mixin private ActionResolverMixin.RequiredParameter actionResolver; @Mixin private CommandHelperMixin commandHelper; @Mixin private ActionValidationMixin actionValidationMixin; + // We explicitly ignore any unknown CLI args, to allow for easy switching between run and help commands. + @Unmatched private String[] actionArgs; @Override public Integer call() throws Exception { + initMixins(); var validationHandler = actionValidationMixin.getActionValidationHandler(); - var actionLoadResult = actionResolver.load("FoD", validationHandler); - ActionCommand - .asCommandLine(commandHelper.getCommandSpec(), actionLoadResult.getAction()) - .usage(System.out); + var action = actionResolver.load("FoD", validationHandler).getAction(); + FoDActionHelper.builder() + .action(action) + .onUnknownParameterType(OnUnknownParameterType.WARN) + .build() + .createCommandLine().usage(System.out); return 0; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/TestCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/TestCommand.java index bbe27c39f9..bdee3c427f 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/TestCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/TestCommand.java @@ -14,8 +14,10 @@ import com.fortify.cli.common.action.cli.mixin.ActionResolverMixin; import com.fortify.cli.common.action.cli.mixin.ActionValidationMixin; +import com.fortify.cli.common.action.helper.ActionParameterHelper.OnUnknownParameterType; import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.cli.mixin.CommandHelperMixin; +import com.fortify.cli.fod.action.helper.FoDActionHelper; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; @@ -31,9 +33,12 @@ public class TestCommand extends AbstractRunnableCommand { @Override public Integer call() throws Exception { var validationHandler = actionValidationMixin.getActionValidationHandler(); - var actionLoadResult = actionResolver.load("FoD", validationHandler); - return ActionCommand - .asCommandLine(commandHelper.getCommandSpec(), actionLoadResult.getAction()) + var action = actionResolver.load("FoD", validationHandler).getAction(); + return FoDActionHelper.builder() + .action(action) + .onUnknownParameterType(OnUnknownParameterType.WARN) + .build() + .createCommandLine() .execute(actionArgs); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/helper/FoDActionHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/helper/FoDActionHelper.java new file mode 100644 index 0000000000..b5e07ec26d --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/helper/FoDActionHelper.java @@ -0,0 +1,56 @@ +/** + * Copyright 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.fod.action.helper; + +import com.fortify.cli.common.action.helper.ActionCommandLineFactory; +import com.fortify.cli.common.action.helper.ActionParameterHelper; +import com.fortify.cli.common.action.helper.ActionParameterHelper.OnUnknownParameterType; +import com.fortify.cli.common.action.helper.ActionParameterHelper.ParameterValueSupplier; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.runner.ActionRunnerCommand; + +import lombok.Builder; +import picocli.CommandLine; + +@Builder +public final class FoDActionHelper { + private final Action action; + private final OnUnknownParameterType onUnknownParameterType; + + public final CommandLine createCommandLine() { + var actionParameterHelper = createActionParameterHelper(); + return ActionCommandLineFactory.builder() + .action(action) + .actionParameterHelper(actionParameterHelper) + .actionRunnerCommand(createActionRunnerCommand(actionParameterHelper)) + .runCmd("fcli fod action run") + .build() + .createCommandLine(); + } + + private final ActionParameterHelper createActionParameterHelper() { + return ActionParameterHelper.builder() + .action(action) + .onUnknownParameterType(onUnknownParameterType) + .optionSpecTypeConfigurer("release_single", (b,p)->ParameterValueSupplier.configure(b, null)) //TODO + .build(); + } + + private final ActionRunnerCommand createActionRunnerCommand(ActionParameterHelper actionParameterHelper) { + return ActionRunnerCommand.builder() + .action(action) + .actionParameterHelper(actionParameterHelper) + .build(); + } + +}