-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
493 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
...i-common/src/main/java/com/fortify/cli/common/action/helper/ActionCommandLineFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
181 changes: 181 additions & 0 deletions
181
...fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.