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 a0e06f81..238a28c3 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 @@ -46,7 +46,17 @@ public interface QCodeExecutor ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such ** *******************************************************************************/ - default Object convertObjectToJava(Object object) + default Object convertObjectToJava(Object object) throws QCodeException + { + return (object); + } + + /******************************************************************************* + ** Convert a native java object into one for the script's language/runtime. + ** e.g., a java Instant to a Nashorn Date + ** + *******************************************************************************/ + default Object convertJavaObject(Object object, Object requestedTypeHint) throws QCodeException { return (object); } 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 898e330f..d4e930dc 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 @@ -29,6 +29,7 @@ import javax.script.ScriptException; import java.io.Serializable; import java.math.BigDecimal; import java.time.Instant; +import java.time.temporal.ChronoField; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -74,72 +75,108 @@ public class QJavaScriptExecutor implements QCodeExecutor ** *******************************************************************************/ @Override - public Object convertObjectToJava(Object object) + public Object convertObjectToJava(Object object) throws QCodeException { - if(object == null || object instanceof String || object instanceof Boolean || object instanceof Integer || object instanceof Long || object instanceof BigDecimal) + try { - 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) - { - try + if(object == null || object instanceof String || object instanceof Boolean || object instanceof Integer || object instanceof Long || object instanceof BigDecimal) { - if("Date".equals(scriptObjectMirror.getClassName())) - { - //////////////////////////////////////////////////////////////////// - // looks like the js Date is in UTC (is that because our JVM is?) // - // so the instant being in UTC matches // - //////////////////////////////////////////////////////////////////// - Double millis = (Double) scriptObjectMirror.callMember("getTime"); - Instant instant = Instant.ofEpochMilli(millis.longValue()); - return (instant); - } + return (object); } - catch(Exception e) + else if(object instanceof Float f) { - LOG.debug("Error unwrapping javascript date", e); + 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(scriptObjectMirror.isArray()) + if(object instanceof ScriptObjectMirror scriptObjectMirror) { - List result = new ArrayList<>(); - for(String key : scriptObjectMirror.keySet()) + try { - result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key))); + if("Date".equals(scriptObjectMirror.getClassName())) + { + //////////////////////////////////////////////////////////////////// + // looks like the js Date is in UTC (is that because our JVM is?) // + // so the instant being in UTC matches // + //////////////////////////////////////////////////////////////////// + Double millis = (Double) scriptObjectMirror.callMember("getTime"); + Instant instant = Instant.ofEpochMilli(millis.longValue()); + return (instant); + } } - return (result); - } - else - { - /////////////////////////////////////////////////////////////////////////////////////////////////////// - // last thing we know to try (though really, there's probably some check we should have around this) // - /////////////////////////////////////////////////////////////////////////////////////////////////////// - Map result = new HashMap<>(); - for(String key : scriptObjectMirror.keySet()) + catch(Exception e) { - result.put(key, convertObjectToJava(scriptObjectMirror.get(key))); + LOG.debug("Error unwrapping javascript date", e); } - return (result); - } - } - return QCodeExecutor.super.convertObjectToJava(object); + if(scriptObjectMirror.isArray()) + { + List result = new ArrayList<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + else + { + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // last thing we know to try (though really, there's probably some check we should have around this) // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + Map result = new HashMap<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.put(key, convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + } + + return QCodeExecutor.super.convertObjectToJava(object); + } + catch(Exception e) + { + throw (new QCodeException("Error converting java object", e)); + } + } + + + + /******************************************************************************* + ** Convert a native java object into one for the script's language/runtime. + ** e.g., a java Instant to a Nashorn Date + ** + *******************************************************************************/ + public Object convertJavaObject(Object object, Object requestedTypeHint) throws QCodeException + { + try + { + if("Date".equals(requestedTypeHint)) + { + if(object instanceof Instant i) + { + long millis = (i.getEpochSecond() * 1000 + i.getLong(ChronoField.MILLI_OF_SECOND)); + ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); + return engine.eval("new Date(" + millis + ")"); + } + } + + return (QCodeExecutor.super.convertJavaObject(object, requestedTypeHint)); + } + catch(Exception e) + { + throw (new QCodeException("Error converting java object", e)); + } } 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 e48dc604..94f128bb 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 @@ -41,9 +41,11 @@ import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.assertj.core.api.Assertions; import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; +import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -284,6 +286,27 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertJavaObject() throws QException + { + TestQCodeExecutorAware converter = new TestQCodeExecutorAware(); + + Instant originalInstant = Instant.parse("2023-07-03T11:42:42Z"); + testOne(1, """ + converter.convertJavaObject("jsDate", instant, "Date"); + converter.convertObject("backToInstant", converter.getConvertedObject("jsDate")); + """, MapBuilder.of("converter", converter, "instant", originalInstant)); + + assertThat(converter.getConvertedObject("jsDate")).isInstanceOf(ScriptObjectMirror.class); + assertEquals(originalInstant, converter.getConvertedObject("backToInstant")); + assertNotSame(originalInstant, converter.getConvertedObject("backToInstant")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -355,13 +378,23 @@ class ExecuteCodeActionTest extends BaseTest /******************************************************************************* ** *******************************************************************************/ - public void convertObject(String name, Object inputObject) + public void convertObject(String name, Object inputObject) throws QCodeException { convertedObjectMap.put(name, qCodeExecutor.convertObjectToJava(inputObject)); } + /******************************************************************************* + ** + *******************************************************************************/ + public void convertJavaObject(String name, Object inputObject, Object hint) throws QCodeException + { + convertedObjectMap.put(name, qCodeExecutor.convertJavaObject(inputObject, hint)); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 5c700b80..a833fe59 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 @@ -36,6 +36,7 @@ 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.QCodeException; 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; @@ -208,7 +209,7 @@ public class ApiScriptUtils implements QCodeExecutorAware, Serializable ** 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) + private Object processBodyToJsonString(Object body) throws QCodeException { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if the caller already supplied the object as a string, then return that string. // @@ -234,7 +235,7 @@ public class ApiScriptUtils implements QCodeExecutorAware, Serializable ** 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) + private Object processInputObjectViaCodeExecutor(Object body) throws QCodeException { if(qCodeExecutor == null || body == null) {