diff --git a/.gitignore b/.gitignore
index a1c2a238..39736a21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,10 @@
+target/
+*.iml
+
+
+#############################################
+## Original contents from github template: ##
+#############################################
# Compiled class file
*.class
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 00000000..fadf2cda
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..a3e1e473
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,129 @@
+
+
+ 4.0.0
+
+ com.kingsrook.qqq
+ qqq-middleware-picocli
+ 0.0-SNAPSHOT
+
+
+
+
+
+
+ UTF-8
+ UTF-8
+ 17
+ 17
+ true
+ true
+
+
+
+
+
+ com.kingsrook.qqq
+ qqq-backend-core
+ 0.0-SNAPSHOT
+
+
+ com.kingsrook.qqq
+ qqq-backend-module-rdbms
+ 0.0-SNAPSHOT
+ test
+
+
+
+
+ info.picocli
+ picocli
+ 4.6.1
+
+
+ com.h2database
+ h2
+ 1.4.197
+ test
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.1.2
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.14.1
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.14.1
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.1
+ test
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ -Xlint:unchecked
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.1.2
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 9.0
+
+
+
+
+ validate
+ validate
+
+ checkstyle.xml
+
+ UTF-8
+ true
+ false
+ true
+ warning
+ **/target/generated-sources/*.*
+
+
+
+ check
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
new file mode 100644
index 00000000..6017c827
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
@@ -0,0 +1,489 @@
+package com.kingsrook.qqq.frontend.picocli;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.MetaDataAction;
+import com.kingsrook.qqq.backend.core.actions.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
+import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter;
+import com.kingsrook.qqq.backend.core.adapters.JsonToQFieldMappingAdapter;
+import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractQFieldMapping;
+import com.kingsrook.qqq.backend.core.model.actions.InsertRequest;
+import com.kingsrook.qqq.backend.core.model.actions.InsertResult;
+import com.kingsrook.qqq.backend.core.model.actions.MetaDataRequest;
+import com.kingsrook.qqq.backend.core.model.actions.MetaDataResult;
+import com.kingsrook.qqq.backend.core.model.actions.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.QueryRequest;
+import com.kingsrook.qqq.backend.core.model.actions.QueryResult;
+import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataRequest;
+import com.kingsrook.qqq.backend.core.model.actions.TableMetaDataResult;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import picocli.CommandLine;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Model.OptionSpec;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.ParseResult;
+import picocli.CommandLine.UnmatchedArgumentException;
+
+
+/*******************************************************************************
+ ** Note: Please do not use System.out or .err here -- rather, use the CommandLine
+ ** object's out & err members - so the unit test can see the output!
+ *
+ *******************************************************************************/
+public class QPicoCliImplementation
+{
+ private static final Logger LOG = LogManager.getLogger(QPicoCliImplementation.class);
+
+ public static final int DEFAULT_LIMIT = 20;
+
+ private static QInstance qInstance;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void main(String[] args)
+ {
+ QInstance qInstance = new QInstance();
+
+ // todo - parse args to look up metaData and prime instance
+ // todo - authentication
+ // qInstance.addBackend(QMetaDataProvider.getQBackend());
+
+ QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance);
+ int exitCode = qPicoCliImplementation.runCli("qapi", args);
+ System.exit(exitCode);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QPicoCliImplementation(QInstance qInstance)
+ {
+ QPicoCliImplementation.qInstance = qInstance;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public int runCli(String name, String[] args)
+ {
+ return (runCli(name, args, System.out, System.err));
+ }
+
+
+
+ /*******************************************************************************
+ ** examples - todo, make docs complete!
+ ** my-app-cli [--all] [--format=]
+ ** my-app-cli $table meta-data [--format=]
+ ** my-app-cli $table query [--filterId=]|[--filter=]|[--criteria=...]
+ ** my-app-cli $table get (--primaryKey=|--$uc=...)
+ ** my-app-cli $table delete (--primaryKey=|--$uc=...)
+ ** my-app-cli $table insert (--body=|--$field=...)
+ ** my-app-cli $table update (--primaryKey=|--$uc=...) (--body=|--$field=...)
+ **
+ *******************************************************************************/
+ public int runCli(String name, String[] args, PrintStream out, PrintStream err)
+ {
+ //////////////////////////////////
+ // define the top-level command //
+ //////////////////////////////////
+ CommandSpec topCommandSpec = CommandSpec.create();
+ topCommandSpec.name(name);
+ topCommandSpec.version(name + " v1.0"); // todo... uh?
+ topCommandSpec.mixinStandardHelpOptions(true); // usageHelp and versionHelp options
+ topCommandSpec.addOption(OptionSpec.builder("-m", "--meta-data")
+ .type(boolean.class)
+ .description("Output the meta-data for this CLI")
+ .build());
+
+ /////////////////////////////////////
+ // add each table as a sub-command //
+ /////////////////////////////////////
+ qInstance.getTables().keySet().stream().sorted().forEach(tableName ->
+ {
+ QTableMetaData table = qInstance.getTable(tableName);
+
+ CommandSpec tableCommand = CommandSpec.create();
+ topCommandSpec.addSubcommand(table.getName(), tableCommand);
+
+ ///////////////////////////////////////////////////
+ // add table-specific sub-commands for the table //
+ ///////////////////////////////////////////////////
+ tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table));
+ tableCommand.addSubcommand("query", defineQueryCommand(table));
+ tableCommand.addSubcommand("insert", defineInsertCommand(table));
+ });
+
+ CommandLine commandLine = new CommandLine(topCommandSpec);
+ commandLine.setOut(new PrintWriter(out, true));
+ commandLine.setErr(new PrintWriter(err, true));
+
+ try
+ {
+ ParseResult parseResult = commandLine.parseArgs(args);
+
+ ///////////////////////////////////////////
+ // Did user request usage help (--help)? //
+ ///////////////////////////////////////////
+ if(commandLine.isUsageHelpRequested())
+ {
+ commandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+ ////////////////////////////////////////////////
+ // Did user request version help (--version)? //
+ ////////////////////////////////////////////////
+ else if(commandLine.isVersionHelpRequested())
+ {
+ commandLine.printVersionHelp(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnVersionHelp();
+ }
+
+ ///////////////////////////
+ // else, run the command //
+ ///////////////////////////
+ return run(commandLine, parseResult);
+ }
+ catch(ParameterException ex)
+ {
+ //////////////////////////////////////////////////
+ // handle command-line/param parsing exceptions //
+ //////////////////////////////////////////////////
+ commandLine.getErr().println(ex.getMessage());
+ UnmatchedArgumentException.printSuggestions(ex, commandLine.getErr());
+ ex.getCommandLine().usage(commandLine.getErr());
+ return commandLine.getCommandSpec().exitCodeOnInvalidInput();
+ }
+ catch(Exception ex)
+ {
+ ///////////////////////////////////////////
+ // handle exceptions from business logic //
+ ///////////////////////////////////////////
+ commandLine.getErr().println("Error: " + ex.getMessage());
+ return (commandLine.getCommandSpec().exitCodeOnExecutionException());
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private CommandSpec defineMetaDataCommand(QTableMetaData table)
+ {
+ return CommandSpec.create();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private CommandSpec defineQueryCommand(QTableMetaData table)
+ {
+ CommandSpec queryCommand = CommandSpec.create();
+ queryCommand.addOption(OptionSpec.builder("-l", "--limit")
+ .type(int.class)
+ .build());
+ queryCommand.addOption(OptionSpec.builder("-s", "--skip")
+ .type(int.class)
+ .build());
+ queryCommand.addOption(OptionSpec.builder("-c", "--criteria")
+ .type(String[].class)
+ .build());
+
+ // todo - add the fields as explicit params?
+
+ return queryCommand;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @SuppressWarnings("checkstyle:Indentation")
+ private CommandSpec defineInsertCommand(QTableMetaData table)
+ {
+ CommandSpec insertCommand = CommandSpec.create();
+
+ insertCommand.addOption(OptionSpec.builder("--jsonBody")
+ .type(String.class)
+ .build());
+
+ insertCommand.addOption(OptionSpec.builder("--jsonFile")
+ .type(String.class)
+ .build());
+
+ insertCommand.addOption(OptionSpec.builder("--csvFile")
+ .type(String.class)
+ .build());
+
+ insertCommand.addOption(OptionSpec.builder("--mapping")
+ .type(String.class)
+ .build());
+
+ for(QFieldMetaData field : table.getFields().values())
+ {
+ insertCommand.addOption(OptionSpec.builder("--field-" + field.getName())
+ .type(
+ switch(field.getType())
+ {
+ case STRING, TEXT, HTML, PASSWORD -> String.class;
+ case INTEGER -> Integer.class;
+ case DECIMAL -> BigDecimal.class;
+ case DATE -> LocalDate.class;
+ case TIME -> LocalTime.class;
+ case DATE_TIME -> LocalDateTime.class;
+ default -> throw new IllegalStateException("Unsupported field type: " + field.getType());
+ }
+ )
+ .build());
+ }
+ return insertCommand;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int run(CommandLine commandLine, ParseResult parseResult) throws QException
+ {
+ if(!parseResult.hasSubcommand())
+ {
+ return runTopLevelCommand(commandLine, parseResult);
+ }
+ else
+ {
+ String subCommandName = parseResult.subcommand().commandSpec().name();
+ CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName);
+ return runTableLevelCommand(subCommandLine, parseResult.subcommand());
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTableLevelCommand(CommandLine commandLine, ParseResult tableParseResult) throws QException
+ {
+ String tableName = tableParseResult.commandSpec().name();
+
+ if(tableParseResult.hasSubcommand())
+ {
+ ParseResult subParseResult = tableParseResult.subcommand();
+ switch(subParseResult.commandSpec().name())
+ {
+ case "meta-data":
+ {
+ return runTableMetaData(commandLine, tableName, subParseResult);
+ }
+ case "query":
+ {
+ return runTableQuery(commandLine, tableName, subParseResult);
+ }
+ case "insert":
+ {
+ return runTableInsert(commandLine, tableName, subParseResult);
+ }
+ default:
+ {
+ commandLine.getErr().println("Unknown command: " + subParseResult.commandSpec().name());
+ commandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+ }
+ }
+ else
+ {
+ commandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTableMetaData(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
+ {
+ TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance);
+ tableMetaDataRequest.setTableName(tableName);
+ TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
+ TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(tableMetaDataResult));
+ return commandLine.getCommandSpec().exitCodeOnSuccess();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTableQuery(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
+ {
+ QueryRequest queryRequest = new QueryRequest(qInstance);
+ queryRequest.setTableName(tableName);
+ queryRequest.setSkip(subParseResult.matchedOptionValue("skip", null));
+ queryRequest.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT));
+
+ QQueryFilter filter = new QQueryFilter();
+ queryRequest.setFilter(filter);
+
+ String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
+ for(String criterion : criteria)
+ {
+ // todo - parse!
+ String[] parts = criterion.split(" ");
+ QFilterCriteria qQueryCriteria = new QFilterCriteria();
+ qQueryCriteria.setFieldName(parts[0]);
+ qQueryCriteria.setOperator(QCriteriaOperator.valueOf(parts[1]));
+ qQueryCriteria.setValues(List.of(parts[2]));
+ filter.addCriteria(qQueryCriteria);
+ }
+
+ QueryAction queryAction = new QueryAction();
+ QueryResult queryResult = queryAction.execute(queryRequest);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(queryResult));
+ return commandLine.getCommandSpec().exitCodeOnSuccess();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTableInsert(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException
+ {
+ InsertRequest insertRequest = new InsertRequest(qInstance);
+ insertRequest.setTableName(tableName);
+ QTableMetaData table = qInstance.getTable(tableName);
+
+ AbstractQFieldMapping> mapping = null;
+
+ if(subParseResult.hasMatchedOption("--mapping"))
+ {
+ String json = subParseResult.matchedOptionValue("--mapping", "");
+ mapping = new JsonToQFieldMappingAdapter().buildMappingFromJson(json);
+ }
+
+ /////////////////////////////////////////////
+ // get the records that the user specified //
+ /////////////////////////////////////////////
+ List recordList = null;
+ if(subParseResult.hasMatchedOption("--jsonBody"))
+ {
+ String json = subParseResult.matchedOptionValue("--jsonBody", "");
+ recordList = new JsonToQRecordAdapter().buildRecordsFromJson(json, table, mapping);
+ }
+ else if(subParseResult.hasMatchedOption("--jsonFile"))
+ {
+ try
+ {
+ String path = subParseResult.matchedOptionValue("--jsonFile", "");
+ String json = FileUtils.readFileToString(new File(path));
+ recordList = new JsonToQRecordAdapter().buildRecordsFromJson(json, table, mapping);
+ }
+ catch(IOException e)
+ {
+ throw (new QException("Error building records from file:" + e.getMessage(), e));
+ }
+ }
+ else if(subParseResult.hasMatchedOption("--csvFile"))
+ {
+ try
+ {
+ String path = subParseResult.matchedOptionValue("--csvFile", "");
+ String csv = FileUtils.readFileToString(new File(path));
+ recordList = new CsvToQRecordAdapter().buildRecordsFromCsv(csv, table, mapping);
+ }
+ catch(IOException e)
+ {
+ throw (new QException("Error building records from file:" + e.getMessage(), e));
+ }
+ }
+ else
+ {
+ QRecord record = new QRecord();
+ recordList = new ArrayList<>();
+ recordList.add(record);
+
+ boolean anyFields = false;
+ for(OptionSpec matchedOption : subParseResult.matchedOptions())
+ {
+ if(matchedOption.longestName().startsWith("--field-"))
+ {
+ anyFields = true;
+ String fieldName = matchedOption.longestName().substring(8);
+ record.setValue(fieldName, matchedOption.getValue());
+ }
+ }
+
+ if(!anyFields)
+ {
+ CommandLine subCommandLine = commandLine.getSubcommands().get("insert");
+ subCommandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+ }
+
+ insertRequest.setRecords(recordList);
+
+ InsertAction insertAction = new InsertAction();
+ InsertResult insertResult = insertAction.execute(insertRequest);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(insertResult));
+ return commandLine.getCommandSpec().exitCodeOnSuccess();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int runTopLevelCommand(CommandLine commandLine, ParseResult parseResult) throws QException
+ {
+ if(parseResult.hasMatchedOption("--meta-data"))
+ {
+ MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance);
+ MetaDataAction metaDataAction = new MetaDataAction();
+ MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest);
+ commandLine.getOut().println(JsonUtils.toPrettyJson(metaDataResult));
+ return commandLine.getCommandSpec().exitCodeOnSuccess();
+ }
+
+ commandLine.usage(commandLine.getOut());
+ return commandLine.getCommandSpec().exitCodeOnUsageHelp();
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
new file mode 100644
index 00000000..5d132429
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java
@@ -0,0 +1,308 @@
+package com.kingsrook.qqq.frontend.picocli;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+class QPicoCliImplementationTest
+{
+ private static final boolean VERBOSE = true;
+ private static final String CLI_NAME = "cli-unit-test";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ TestUtils.primeTestDatabase();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_noArgs()
+ {
+ TestOutput testOutput = testCli();
+ assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_help()
+ {
+ TestOutput testOutput = testCli("--help");
+ assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME));
+ assertTrue(testOutput.getOutput().matches("(?s).*Commands:.*person.*"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_version()
+ {
+ TestOutput testOutput = testCli("--version");
+ assertTrue(testOutput.getOutput().contains(CLI_NAME + " v1.0"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_badOption()
+ {
+ String badOption = "--asdf";
+ TestOutput testOutput = testCli(badOption);
+ assertTrue(testOutput.getError().contains("Unknown option: '" + badOption + "'"));
+ assertTrue(testOutput.getError().contains("Usage: " + CLI_NAME));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_metaData()
+ {
+ TestOutput testOutput = testCli("--meta-data");
+ JSONObject metaData = JsonUtils.toJSONObject(testOutput.getOutput());
+ assertNotNull(metaData);
+ assertEquals(1, metaData.keySet().size(), "Number of top-level keys");
+ assertTrue(metaData.has("tables"));
+ JSONObject tables = metaData.getJSONObject("tables");
+ JSONObject personTable = tables.getJSONObject("person");
+ assertEquals("person", personTable.getString("name"));
+ assertEquals("Person", personTable.getString("label"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_table()
+ {
+ TestOutput testOutput = testCli("person");
+ assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME + " person [COMMAND]"));
+ assertTrue(testOutput.getOutput().matches("(?s).*Commands:.*query.*"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableMetaData()
+ {
+ TestOutput testOutput = testCli("person", "meta-data");
+ JSONObject metaData = JsonUtils.toJSONObject(testOutput.getOutput());
+ assertNotNull(metaData);
+ assertEquals(1, metaData.keySet().size(), "Number of top-level keys");
+ JSONObject table = metaData.getJSONObject("table");
+ assertEquals(4, table.keySet().size(), "Number of mid-level keys");
+ assertEquals("person", table.getString("name"));
+ assertEquals("Person", table.getString("label"));
+ assertEquals("id", table.getString("primaryKeyField"));
+ JSONObject fields = table.getJSONObject("fields");
+ JSONObject field0 = fields.getJSONObject("id");
+ assertEquals("id", field0.getString("name"));
+ assertEquals("INTEGER", field0.getString("type"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableQuery()
+ {
+ TestOutput testOutput = testCli("person", "query", "--skip=1", "--limit=2", "--criteria", "id NOT_EQUALS 3");
+ JSONObject queryResult = JsonUtils.toJSONObject(testOutput.getOutput());
+ assertNotNull(queryResult);
+ assertEquals(2, queryResult.getJSONArray("records").length());
+ // query for id != 3, and skipping 1, expect to get back rows 2 & 4
+ assertEquals(2, queryResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey"));
+ assertEquals(4, queryResult.getJSONArray("records").getJSONObject(1).getInt("primaryKey"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private TestOutput testCli(String... args)
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance);
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
+
+ if(VERBOSE)
+ {
+ System.out.println("> " + CLI_NAME + (args == null ? "" : " " + StringUtils.join(" ", Arrays.stream(args).toList())));
+ }
+
+ qPicoCliImplementation.runCli(CLI_NAME, args, new PrintStream(outputStream, true), new PrintStream(errorStream, true));
+
+ String output = outputStream.toString(StandardCharsets.UTF_8);
+ String error = errorStream.toString(StandardCharsets.UTF_8);
+
+ if(VERBOSE)
+ {
+ System.out.println(output);
+ System.err.println(error);
+ }
+
+ TestOutput testOutput = new TestOutput(output, error);
+ return (testOutput);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static class TestOutput
+ {
+ private String output;
+ private String[] outputLines;
+ private String error;
+ private String[] errorLines;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public TestOutput(String output, String error)
+ {
+ this.output = output;
+ this.error = error;
+
+ this.outputLines = output.split("\n");
+ this.errorLines = error.split("\n");
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for output
+ **
+ *******************************************************************************/
+ public String getOutput()
+ {
+ return output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for output
+ **
+ *******************************************************************************/
+ public void setOutput(String output)
+ {
+ this.output = output;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for outputLines
+ **
+ *******************************************************************************/
+ public String[] getOutputLines()
+ {
+ return outputLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for outputLines
+ **
+ *******************************************************************************/
+ public void setOutputLines(String[] outputLines)
+ {
+ this.outputLines = outputLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for error
+ **
+ *******************************************************************************/
+ public String getError()
+ {
+ return error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for error
+ **
+ *******************************************************************************/
+ public void setError(String error)
+ {
+ this.error = error;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for errorLines
+ **
+ *******************************************************************************/
+ public String[] getErrorLines()
+ {
+ return errorLines;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for errorLines
+ **
+ *******************************************************************************/
+ public void setErrorLines(String[] errorLines)
+ {
+ this.errorLines = errorLines;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
new file mode 100644
index 00000000..622d435a
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
@@ -0,0 +1,94 @@
+package com.kingsrook.qqq.frontend.picocli;
+
+
+import java.io.InputStream;
+import java.sql.Connection;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
+import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
+import org.apache.commons.io.IOUtils;
+import static junit.framework.Assert.assertNotNull;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TestUtils
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @SuppressWarnings("unchecked")
+ public static void primeTestDatabase() throws Exception
+ {
+ ConnectionManager connectionManager = new ConnectionManager();
+ Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(TestUtils.defineBackend()));
+ InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
+ assertNotNull(primeTestDatabaseSqlStream);
+ List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream);
+ String joinedSQL = String.join("\n", lines);
+ for(String sql : joinedSQL.split(";"))
+ {
+ QueryManager.executeUpdate(connection, sql);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QInstance defineInstance()
+ {
+ QInstance qInstance = new QInstance();
+ qInstance.addBackend(defineBackend());
+ qInstance.addTable(defineTablePerson());
+ return (qInstance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QBackendMetaData defineBackend()
+ {
+ return new QBackendMetaData()
+ .withName("default")
+ .withType("rdbms")
+ .withValue("vendor", "h2")
+ .withValue("hostName", "mem")
+ .withValue("databaseName", "test_database")
+ .withValue("username", "sa")
+ .withValue("password", "");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QTableMetaData defineTablePerson()
+ {
+ return new QTableMetaData()
+ .withName("person")
+ .withLabel("Person")
+ .withBackendName(defineBackend().getName())
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date"))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date"))
+ .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name"))
+ .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name"))
+ .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
+ .withField(new QFieldMetaData("email", QFieldType.STRING));
+ }
+
+}
diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql
new file mode 100644
index 00000000..e5adab34
--- /dev/null
+++ b/src/test/resources/prime-test-database.sql
@@ -0,0 +1,18 @@
+DROP TABLE IF EXISTS person;
+CREATE TABLE person
+(
+ id SERIAL,
+ create_date TIMESTAMP DEFAULT now(),
+ modify_date TIMESTAMP DEFAULT now(),
+
+ first_name VARCHAR(80) NOT NULL,
+ last_name VARCHAR(80) NOT NULL,
+ birth_date DATE,
+ email VARCHAR(250) NOT NULL
+);
+
+INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com');
+INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com');
+INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com');
+INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (4, 'Tyler', 'Samples', '1990-01-01', 'tsamples@mmltholdings.com');
+INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com');