Skip to content

Commit

Permalink
chore: Partial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rsenden committed Sep 20, 2024
1 parent 695b998 commit efb377a
Show file tree
Hide file tree
Showing 16 changed files with 493 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<Integer> {
@Override
public Integer call() throws Exception {
return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, BiConsumer<OptionSpec.Builder, ActionParameter>> 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<ActionParameter, ParameterTypeConverterArgs> 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<String>();
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<OptionSpec.Builder, ActionParameter> 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("<unknown>");
}

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<ArrayNode> {
@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<String, ParameterTypeConverterArgs, JsonNode> converter;

public JsonNode getValue(ParameterTypeConverterArgs args) {
return converter.apply(value, args);
}

public static final OptionSpec.Builder configure(OptionSpec.Builder builder, BiFunction<String, ParameterTypeConverterArgs, JsonNode> converter) {
builder.arity("1").type(ParameterValueSupplier.class).converters(value->new ParameterValueSupplier(value, converter));
return builder;
}
}
}
Loading

0 comments on commit efb377a

Please sign in to comment.