From 3791c069c7c6e210eca69f8b422863d796e0eded Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 09:06:57 -0500 Subject: [PATCH] Add convertObjectToJava to code executors - for converting language objects to java objects --- .../actions/scripts/ExecuteCodeAction.java | 11 +++ .../core/actions/scripts/QCodeExecutor.java | 10 ++ .../actions/scripts/QCodeExecutorAware.java | 36 +++++++ .../javascript/QJavaScriptExecutor.java | 59 +++++++++++ .../javascript/ExecuteCodeActionTest.java | 97 +++++++++++++++++++ .../qqq/api/utils/ApiScriptUtils.java | 65 ++++++++++++- 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java index c142fc1f..875cf2f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -101,6 +101,17 @@ public class ExecuteCodeAction context.putAll(input.getInput()); } + ///////////////////////////////////////////////////////////////////////////////// + // set the qCodeExecutor into any context objects which are QCodeExecutorAware // + ///////////////////////////////////////////////////////////////////////////////// + for(Serializable value : context.values()) + { + if(value instanceof QCodeExecutorAware qCodeExecutorAware) + { + qCodeExecutorAware.setQCodeExecutor(qCodeExecutor); + } + } + Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger); output.setOutput(codeOutput); executionLogger.acceptExecutionEnd(codeOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java index e6cd808a..a0e06f81 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java @@ -41,4 +41,14 @@ public interface QCodeExecutor *******************************************************************************/ Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException; + /******************************************************************************* + ** Process an object from the script's language/runtime into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + ** + *******************************************************************************/ + default Object convertObjectToJava(Object object) + { + return (object); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java new file mode 100644 index 00000000..e15dd2a6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java @@ -0,0 +1,36 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.scripts; + + +/******************************************************************************* + ** Interface for classes that can accept a QCodeExecutor object via a setter. + *******************************************************************************/ +public interface QCodeExecutorAware +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void setQCodeExecutor(QCodeExecutor qCodeExecutor); + +} diff --git a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java index 15f7ac98..7a708ba7 100644 --- a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java +++ b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java @@ -27,6 +27,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; @@ -36,8 +40,10 @@ import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.NotImplementedException; import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; +import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import org.openjdk.nashorn.internal.runtime.ECMAException; import org.openjdk.nashorn.internal.runtime.ParserException; +import org.openjdk.nashorn.internal.runtime.Undefined; /******************************************************************************* @@ -59,6 +65,59 @@ public class QJavaScriptExecutor implements QCodeExecutor + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object convertObjectToJava(Object object) + { + if(object == null || object instanceof String || object instanceof Boolean || object instanceof Integer || object instanceof Long || object instanceof BigDecimal) + { + return (object); + } + else if(object instanceof Float f) + { + return (new BigDecimal(f)); + } + else if(object instanceof Double d) + { + return (new BigDecimal(d)); + } + else if(object instanceof Undefined) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // well, we always said we wanted javascript to treat null & undefined the same way... here's our chance // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (null); + } + + if(object instanceof ScriptObjectMirror scriptObjectMirror) + { + if(scriptObjectMirror.isArray()) + { + List result = new ArrayList<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + else + { + Map result = new HashMap<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.put(key, convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + } + + return QCodeExecutor.super.convertObjectToJava(object); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java index 3c1bf009..43338983 100644 --- a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java @@ -23,7 +23,12 @@ package com.kingsrook.qqq.languages.javascript; import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.exceptions.QCodeException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; @@ -31,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -241,10 +247,50 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertObjectToJava() throws QException + { + TestQCodeExecutorAware converter = new TestQCodeExecutorAware(); + testOne(1, """ + converter.convertObject("one", 1); + converter.convertObject("two", "two"); + converter.convertObject("true", true); + converter.convertObject("null", null); + converter.convertObject("undefined", undefined); + converter.convertObject("flatMap", {"a": 1, "b": "c"}); + converter.convertObject("flatList", ["a", 1, "b", "c"]); + converter.convertObject("mixedMap", {"a": [1, {"2": "3"}], "b": {"c": ["d"]}}); + """, MapBuilder.of("converter", converter)); + + assertEquals(1, converter.getConvertedObject("one")); + assertEquals("two", converter.getConvertedObject("two")); + assertEquals(true, converter.getConvertedObject("true")); + assertNull(converter.getConvertedObject("null")); + assertNull(converter.getConvertedObject("undefined")); + assertEquals(Map.of("a", 1, "b", "c"), converter.getConvertedObject("flatMap")); + assertEquals(List.of("a", 1, "b", "c"), converter.getConvertedObject("flatList")); + assertEquals(Map.of("a", List.of(1, Map.of("2", "3")), "b", Map.of("c", List.of("d"))), converter.getConvertedObject("mixedMap")); + } + + + /******************************************************************************* ** *******************************************************************************/ private OneTestOutput testOne(Integer inputValueC, String code) throws QException + { + return (testOne(inputValueC, code, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private OneTestOutput testOne(Integer inputValueC, String code, Map additionalContext) throws QException { System.out.println(); QInstance instance = TestUtils.defineInstance(); @@ -259,6 +305,14 @@ class ExecuteCodeActionTest extends BaseTest input.withContext("input", testInput); input.withContext("output", testOutput); + if(additionalContext != null) + { + for(Map.Entry entry : additionalContext.entrySet()) + { + input.withContext(entry.getKey(), entry.getValue()); + } + } + ExecuteCodeOutput output = new ExecuteCodeOutput(); ExecuteCodeAction executeCodeAction = new ExecuteCodeAction(); @@ -269,6 +323,49 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + public static class TestQCodeExecutorAware implements QCodeExecutorAware, Serializable + { + private QCodeExecutor qCodeExecutor; + + private Map convertedObjectMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void convertObject(String name, Object inputObject) + { + convertedObjectMap.put(name, qCodeExecutor.convertObjectToJava(inputObject)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Object getConvertedObject(String name) + { + return (convertedObjectMap.get(name)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index f06585d2..af776782 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -32,19 +32,24 @@ import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; /******************************************************************************* ** Object injected into script context, for interfacing with a QQQ API. *******************************************************************************/ -public class ApiScriptUtils implements Serializable +public class ApiScriptUtils implements QCodeExecutorAware, Serializable { private String apiName; private String apiVersion; + private QCodeExecutor qCodeExecutor; + /******************************************************************************* @@ -165,6 +170,7 @@ public class ApiScriptUtils implements Serializable public Map insert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("insert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -176,6 +182,7 @@ public class ApiScriptUtils implements Serializable public List> bulkInsert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkInsert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -187,17 +194,61 @@ public class ApiScriptUtils implements Serializable public void update(String tableApiName, Object primaryKey, Object body) throws QException { validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")"); + body = processBodyToJsonString(body); ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body)); } + /******************************************************************************* + ** Take a "body" object, which maybe defined in the script's language/run-time, + ** and try to process it into a JSON String (which is what the API Implementation wants) + *******************************************************************************/ + private Object processBodyToJsonString(Object body) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the caller already supplied the object as a string, then return that string. // + // and in case it can't be parsed as json, well, let that error come out of the api implementation, and go back to the caller. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(body instanceof String) + { + return (body); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the input body wasn't a json string, try to convert it from a language-type object (e.g., javscript) to a java-object, // + // then make JSON out of that for the APIImplementation // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Object bodyJavaObject = processInputObjectViaCodeExecutor(body); + return JsonUtils.toJson(bodyJavaObject); + } + + + + /******************************************************************************* + ** Use the QCodeExecutor (if we have one) to process an input object from the + ** script's language into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + *******************************************************************************/ + private Object processInputObjectViaCodeExecutor(Object body) + { + if(qCodeExecutor == null || body == null) + { + return (body); + } + + return (qCodeExecutor.convertObjectToJava(body)); + } + + + /******************************************************************************* ** *******************************************************************************/ public List> bulkUpdate(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -220,6 +271,7 @@ public class ApiScriptUtils implements Serializable public List> bulkDelete(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkDelete(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -257,4 +309,15 @@ public class ApiScriptUtils implements Serializable } return paramMap; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } }