Skip to content

Commit

Permalink
chore: Partial data-extract implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rsenden committed Feb 20, 2024
1 parent da6fc5c commit 25d02c6
Show file tree
Hide file tree
Showing 37 changed files with 2,621 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,36 @@
package com.fortify.cli.common.cli.mixin;

import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.function.Consumer;
import java.util.function.Function;

import com.formkiq.graalvm.annotations.Reflectable;
import com.fortify.cli.common.util.PicocliSpecHelper;
import com.fortify.cli.common.util.StringUtils;
import com.github.freva.asciitable.AsciiTable;
import com.github.freva.asciitable.Column;
import com.github.freva.asciitable.HorizontalAlign;

import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import picocli.CommandLine.Command;
import picocli.CommandLine.IParameterPreprocessor;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Model.ArgSpec;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.UnmatchedArgsBinding;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.Spec;

public class CommonOptionMixins {
private CommonOptionMixins() {}
Expand Down Expand Up @@ -78,4 +99,157 @@ private String getPlainPrompt(CommandSpec spec, Object... promptArgs) {
return prompt;
}
}

/**
* This mixin allows for collecting any unmatched command line options
* and parsing them into an [option name]=[value] map.
*/
@Command(preprocessor = OptionParametersMixinPreprocessor.class)
public static final class OptionParametersMixin {
@Getter private Map<String, String> options = new LinkedHashMap<>();
@Getter private Map<String, String> optionNameToArgMap = new LinkedHashMap<>();
@Spec private CommandSpec commandSpec;

public final <T> T setUnmatchedArgs(T arg) {
parse((String[])arg);
return arg;
}

public final void validate(Consumer<OptionParameterValidator> validatorConfigurer) {
var validator = new OptionParameterValidator();
validatorConfigurer.accept(validator);
validator.validate();
}

public final class OptionParameterValidator {
private final Map<String, String> optionsWithDescriptions = new LinkedHashMap<>();
private final Map<String, Function<OptionParameterValidatorArgs, String>> optionValidators = new LinkedHashMap<>();
private final List<String> validationErrors = new ArrayList<>();
@Setter private String unknownOptionFormat = "Unknown option: %s";
@Setter private String validationExceptionFormat = "Invalid options:\n %s\nSupported options:\n%s\n";

public final void addSupportedOption(String name, String description) {
optionsWithDescriptions.put(name, description);
}

public final void addOptionValidator(String name, Function<OptionParameterValidatorArgs, String> validator) {
optionValidators.put(name, validator);
}

public final void requiredOption(String name, String validationErrorFormat) {
addOptionValidator(name, args->StringUtils.isBlank(args.getValue())
? String.format(validationErrorFormat, args.getArg())
: null);
}

public final void validate() {
for ( var e : optionNameToArgMap.entrySet() ) {
if ( !optionsWithDescriptions.containsKey(e.getKey()) ) {
validationErrors.add(String.format(unknownOptionFormat, e.getValue()));
}
}
optionValidators.entrySet().forEach(this::validate);
if ( validationErrors.size()>0 ) {
var errorsString = String.join("\n ", validationErrors);
var supportedOptionsString = getSupportedOptionsTable();
var msg = String.format(validationExceptionFormat, errorsString, supportedOptionsString);
throw new ParameterException(commandSpec.commandLine(), msg);
}
}

private final String getSupportedOptionsTable() {
return AsciiTable.builder()
.border(AsciiTable.NO_BORDERS)
.data(new Column[] {
new Column().dataAlign(HorizontalAlign.LEFT),
new Column().dataAlign(HorizontalAlign.LEFT),
},
optionsWithDescriptions.entrySet().stream()
.map(e->new String[] {asArg(e.getKey()), e.getValue()})
.toList().toArray(String[][]::new))
.asString();
}

private final String asArg(String name) {
var arg = optionNameToArgMap.get(name);
if ( arg==null ) {
arg = name.length()==1 ? "-"+name : "--"+name;
}
return arg;
}

private final void validate(Map.Entry<String, Function<OptionParameterValidatorArgs, String>> entry) {
var name = entry.getKey();
var arg = asArg(name);
var value = options.get(name);
var validationError = entry.getValue().apply(new OptionParameterValidatorArgs(name, arg, value));
if ( StringUtils.isNotBlank(validationError) ) {
validationErrors.add(validationError);
}
}
}

@Data
public static final class OptionParameterValidatorArgs {
private final String name;
private final String arg;
private final String value;
}

private final void parse(String[] argsArray) {
var args = asDeque(argsArray);
while ( !args.isEmpty() ) {
var arg = args.pop();
if ( !arg.startsWith("-") ) {
throw new IllegalArgumentException("Unknown command line option: "+arg);
} else {
var optName = arg.replaceFirst("-+", "");
getOptionNameToArgMap().put(optName, arg);
var nextArg = args.peek();
if ( nextArg==null || nextArg.startsWith("-") ) {
options.put(optName, "");
} else {
options.put(optName, args.pop());
}
}
}
}

private final Deque<String> asDeque(String[] args) {
Deque<String> result = new ArrayDeque<>();
// Split --opt=value into separate args on the Deque
for ( var arg: args ) {
if ( arg.startsWith("--") ) {
// Allow for --opt=val, adding option and value as separate args
var elts = arg.split("=", 2);
result.add(elts[0]);
if ( elts.length==2 ) { result.add(elts[1]); }
} else if ( arg.startsWith("-") ) {
result.add(arg.substring(0,2));
if ( arg.length()>2 ) {
var val = arg.substring(2);
if ( val.startsWith("=") ) { val = val.substring(1); }
result.add(val);
}
} else {
result.add(arg);
}
}
return result;
}
}

@Reflectable @NoArgsConstructor
static final class OptionParametersMixinPreprocessor implements IParameterPreprocessor {
@Override
public final boolean preprocess(Stack<String> args, CommandSpec commandSpec, ArgSpec argSpec, Map<String, Object> info) {
var mixin = commandSpec.mixins().values().stream()
.map(CommandSpec::userObject)
.filter(o->o instanceof OptionParametersMixin)
.map(OptionParametersMixin.class::cast)
.findFirst().get();
commandSpec.addUnmatchedArgsBinding(UnmatchedArgsBinding.forStringArrayConsumer(mixin::setUnmatchedArgs));
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*******************************************************************************
* Copyright 2021, 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.data_extract.cli.cmd;

import java.util.List;

import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand;
import com.fortify.cli.common.cli.mixin.CommonOptionMixins.OptionParametersMixin;
import com.fortify.cli.common.data_extract.helper.DataExtractTemplateExecutor;
import com.fortify.cli.common.data_extract.helper.DataExtractTemplateHelper;
import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin;
import com.fortify.cli.common.util.DisableTest;
import com.fortify.cli.common.util.DisableTest.TestType;

import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;

public abstract class AbstractDataExtractCreateCommand extends AbstractRunnableCommand implements Runnable {
@Option(names={"-t", "--template"}, required=true) private String template;
@DisableTest({TestType.MULTI_OPT_SPLIT, TestType.MULTI_OPT_PLURAL_NAME, TestType.OPT_LONG_NAME})
@Option(names="--<template-parameter>", paramLabel="<value>", descriptionKey="fcli.data-extract.create.template-parameter")
private List<String> dummyForSynopsis;
@Mixin private OptionParametersMixin templateParameters;
@Mixin private ProgressWriterFactoryMixin progressWriterFactory;

@Override
public final void run() {
initMixins();
try ( var progressWriter = progressWriterFactory.create() ) {
progressWriter.writeProgress("Loading template %s", template);
var templateDescriptor = DataExtractTemplateHelper.load(getType(), template);
progressWriter.writeProgress("Executing template %s", template);
try ( var templateExecutor = DataExtractTemplateExecutor.builder()
.template(templateDescriptor)
.inputParameters(templateParameters.getOptions())
.progressWriter(progressWriter).build() )
{
configure(templateExecutor);
templateParameters.validate(templateExecutor::configureOptionParameterValidator);
templateExecutor.execute();
}
}
}

protected abstract String getType();
protected abstract void configure(DataExtractTemplateExecutor templateExecutor);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright 2021, 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.data_extract.cli.cmd;

import com.fasterxml.jackson.databind.JsonNode;
import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand;
import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier;

public abstract class AbstractDataExtractListTemplatesCommand extends AbstractOutputCommand implements IJsonNodeSupplier {
@Override
public final JsonNode getJsonNode() {
// TODO Auto-generated method stub
return null;
}
@Override
public final boolean isSingular() {
return false;
}
protected abstract String getType();


}
Loading

0 comments on commit 25d02c6

Please sign in to comment.