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..e73e5525
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,141 @@
+
+
+ 4.0.0
+
+ com.kingsrook.qqq
+ qqq-middleware-javalin
+ 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
+
+
+
+
+ io.javalin
+ javalin
+ 3.13.10
+
+
+ com.konghq
+ unirest-java
+ 3.4.00
+ test
+
+
+ com.h2database
+ h2
+ 1.4.197
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.30
+ 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/backend/javalin/QJavalinImplementation.java b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
new file mode 100644
index 00000000..13efb344
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -0,0 +1,272 @@
+package com.kingsrook.qqq.backend.javalin;
+
+
+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.exceptions.QUserFacingException;
+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.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.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import io.javalin.Javalin;
+import io.javalin.apibuilder.EndpointGroup;
+import io.javalin.http.Context;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.eclipse.jetty.http.HttpStatus;
+import static io.javalin.apibuilder.ApiBuilder.delete;
+import static io.javalin.apibuilder.ApiBuilder.get;
+import static io.javalin.apibuilder.ApiBuilder.patch;
+import static io.javalin.apibuilder.ApiBuilder.path;
+import static io.javalin.apibuilder.ApiBuilder.post;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QJavalinImplementation
+{
+ private static final Logger LOG = LogManager.getLogger(QJavalinImplementation.class);
+
+ private static QInstance qInstance;
+
+ private static int PORT = 8001;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void main(String[] args)
+ {
+ QInstance qInstance = new QInstance();
+ // todo - parse args to look up metaData and prime instance
+ // qInstance.addBackend(QMetaDataProvider.getQBackend());
+
+ new QJavalinImplementation(qInstance).startJavalinServer(PORT);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QJavalinImplementation(QInstance qInstance)
+ {
+ QJavalinImplementation.qInstance = qInstance;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for qInstance
+ **
+ *******************************************************************************/
+ public static void setQInstance(QInstance qInstance)
+ {
+ QJavalinImplementation.qInstance = qInstance;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ void startJavalinServer(int port)
+ {
+ // todo port from arg
+ // todo base path from arg?
+ Javalin service = Javalin.create().start(port);
+ service.routes(getRoutes());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public EndpointGroup getRoutes()
+ {
+ return (() ->
+ {
+ path("/metaData", () ->
+ {
+ get("/", QJavalinImplementation::metaData);
+ path("/:table", () ->
+ {
+ get("", QJavalinImplementation::tableMetaData);
+ });
+ });
+ path("/data", () ->
+ {
+ path("/:table", () ->
+ {
+ get("/", QJavalinImplementation::dataQuery);
+ post("/", QJavalinImplementation::dataInsert);
+ path("/:id", () ->
+ {
+ get("", QJavalinImplementation::dataGet);
+ patch("", QJavalinImplementation::dataUpdate);
+ delete("", QJavalinImplementation::dataDelete);
+ });
+ });
+ });
+ });
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void dataDelete(Context context)
+ {
+ context.result("{\"deleteResult\":{}}");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void dataUpdate(Context context)
+ {
+ context.result("{\"updateResult\":{}}");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void dataInsert(Context context)
+ {
+ context.result("{\"insertResult\":{}}");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ ********************************************************************************/
+ private static void dataGet(Context context)
+ {
+ context.result("{\"getResult\":{}}");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ static void dataQuery(Context context)
+ {
+ try
+ {
+ QueryRequest queryRequest = new QueryRequest(qInstance);
+ queryRequest.setTableName(context.pathParam("table"));
+ queryRequest.setSkip(integerQueryParam(context, "skip"));
+ queryRequest.setLimit(integerQueryParam(context, "limit"));
+
+ QueryAction queryAction = new QueryAction();
+ QueryResult queryResult = queryAction.execute(queryRequest);
+
+ context.result(JsonUtils.toJson(queryResult));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void metaData(Context context)
+ {
+ try
+ {
+ MetaDataRequest metaDataRequest = new MetaDataRequest(qInstance);
+ MetaDataAction metaDataAction = new MetaDataAction();
+ MetaDataResult metaDataResult = metaDataAction.execute(metaDataRequest);
+
+ context.result(JsonUtils.toJson(metaDataResult));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void tableMetaData(Context context)
+ {
+ try
+ {
+ TableMetaDataRequest tableMetaDataRequest = new TableMetaDataRequest(qInstance);
+ tableMetaDataRequest.setTableName(context.pathParam("table"));
+ TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
+ TableMetaDataResult tableMetaDataResult = tableMetaDataAction.execute(tableMetaDataRequest);
+
+ context.result(JsonUtils.toJson(tableMetaDataResult));
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void handleException(Context context, Exception e)
+ {
+ QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
+ if(userFacingException != null)
+ {
+ LOG.info("User-facing exception", e);
+ context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
+ }
+ else
+ {
+ LOG.warn("Exception in javalin request", e);
+ e.printStackTrace();
+ context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .result("{\"error\":\"" + e.getClass().getSimpleName() + "\"}");
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Returns Integer if context has a valid int query parameter by the given name,
+ * Returns null if no param (or empty value).
+ * Throws NumberFormatException for malformed numbers.
+ *******************************************************************************/
+ private static Integer integerQueryParam(Context context, String name) throws NumberFormatException
+ {
+ String value = context.queryParam(name);
+ if(StringUtils.hasContent(value))
+ {
+ return (Integer.parseInt(value));
+ }
+
+ return (null);
+ }
+}
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
new file mode 100644
index 00000000..426a01f4
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
@@ -0,0 +1,133 @@
+package com.kingsrook.qqq.backend.javalin;
+
+
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import kong.unirest.HttpResponse;
+import kong.unirest.Unirest;
+import org.eclipse.jetty.http.HttpStatus;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeAll;
+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.assertTrue;
+
+
+/*******************************************************************************
+ ** based on https://javalin.io/tutorials/testing
+ **
+ *******************************************************************************/
+class QJavalinImplementationTest
+{
+ private static final int PORT = 6262;
+ private static final String BASE_URL = "http://localhost:" + PORT;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeAll
+ public static void beforeAll() throws Exception
+ {
+ QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance());
+ qJavalinImplementation.startJavalinServer(PORT);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ TestUtils.primeTestDatabase();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_metaData()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData").asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertTrue(jsonObject.has("tables"));
+ JSONObject tables = jsonObject.getJSONObject("tables");
+ assertEquals(1, tables.length());
+ JSONObject table0 = tables.getJSONObject("person");
+ assertTrue(table0.has("name"));
+ assertEquals("person", table0.getString("name"));
+ assertTrue(table0.has("label"));
+ assertEquals("Person", table0.getString("label"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_tableMetaData()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData/person").asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
+ JSONObject table = jsonObject.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_tableMetaData_notFound()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").asString();
+
+ assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
+ String error = jsonObject.getString("error");
+ assertTrue(error.contains("not found"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void test_dataQuery()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/data/person").asString();
+
+ assertEquals(200, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertTrue(jsonObject.has("records"));
+ JSONArray records = jsonObject.getJSONArray("records");
+ assertEquals(5, records.length());
+ JSONObject record0 = records.getJSONObject(0);
+ assertTrue(record0.has("values"));
+ assertEquals("person", record0.getString("tableName"));
+ assertTrue(record0.has("primaryKey"));
+ JSONObject values0 = record0.getJSONObject("values");
+ assertTrue(values0.has("firstName"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
new file mode 100644
index 00000000..5074661b
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -0,0 +1,93 @@
+package com.kingsrook.qqq.backend.javalin;
+
+
+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');