From aa56e52024290d15f76facaadac22bfe2543b0f4 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 8 Nov 2021 21:19:06 -0600 Subject: [PATCH] Initial code checkin --- .gitignore | 7 + checkstyle.xml | 242 ++++++++++++++++ pom.xml | 141 +++++++++ .../javalin/QJavalinImplementation.java | 272 ++++++++++++++++++ .../javalin/QJavalinImplementationTest.java | 133 +++++++++ .../qqq/backend/javalin/TestUtils.java | 93 ++++++ src/test/resources/prime-test-database.sql | 18 ++ 7 files changed, 906 insertions(+) create mode 100644 checkstyle.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java create mode 100644 src/test/resources/prime-test-database.sql 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');