mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-21 06:28:44 +00:00
update to replace nashorn with graalvm javascript engine
This commit is contained in:
@ -47,9 +47,14 @@
|
|||||||
|
|
||||||
<!-- 3rd party deps specifically for this module -->
|
<!-- 3rd party deps specifically for this module -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjdk.nashorn</groupId>
|
<groupId>org.graalvm.js</groupId>
|
||||||
<artifactId>nashorn-core</artifactId>
|
<artifactId>js</artifactId>
|
||||||
<version>15.4</version>
|
<version>22.3.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.graalvm.js</groupId>
|
||||||
|
<artifactId>js-scriptengine</artifactId>
|
||||||
|
<version>22.3.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Common deps for all qqq modules -->
|
<!-- Common deps for all qqq modules -->
|
||||||
|
@ -21,11 +21,9 @@
|
|||||||
|
|
||||||
package com.kingsrook.qqq.languages.javascript;
|
package com.kingsrook.qqq.languages.javascript;
|
||||||
|
|
||||||
|
// Javax imports removed for GraalVM migration
|
||||||
|
|
||||||
|
|
||||||
import javax.script.Bindings;
|
|
||||||
import javax.script.ScriptEngine;
|
|
||||||
import javax.script.ScriptEngineManager;
|
|
||||||
import javax.script.ScriptException;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@ -39,14 +37,11 @@ import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLogg
|
|||||||
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
|
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import org.apache.commons.lang.NotImplementedException;
|
import org.apache.commons.lang.NotImplementedException;
|
||||||
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
|
import org.graalvm.polyglot.Context;
|
||||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
import org.graalvm.polyglot.Source;
|
||||||
import org.openjdk.nashorn.internal.runtime.ECMAException;
|
import org.graalvm.polyglot.Value;
|
||||||
import org.openjdk.nashorn.internal.runtime.ParserException;
|
|
||||||
import org.openjdk.nashorn.internal.runtime.Undefined;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -91,57 +86,85 @@ public class QJavaScriptExecutor implements QCodeExecutor
|
|||||||
{
|
{
|
||||||
return (new BigDecimal(d));
|
return (new BigDecimal(d));
|
||||||
}
|
}
|
||||||
else if(object instanceof Undefined)
|
else if(object instanceof Value val)
|
||||||
{
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// 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
|
try
|
||||||
{
|
{
|
||||||
if("Date".equals(scriptObjectMirror.getClassName()))
|
//////////////////////////////////////////////////
|
||||||
|
// treat JavaScript null/undefined as Java null //
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
if(val.isNull() || val.isHostObject() && val.asHostObject() == null)
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////
|
return null;
|
||||||
// looks like the js Date is in UTC (is that because our JVM is?) //
|
}
|
||||||
// so the instant being in UTC matches //
|
////////////////
|
||||||
////////////////////////////////////////////////////////////////////
|
// primitives //
|
||||||
Double millis = (Double) scriptObjectMirror.callMember("getTime");
|
////////////////
|
||||||
Instant instant = Instant.ofEpochMilli(millis.longValue());
|
if(val.isString())
|
||||||
return (instant);
|
{
|
||||||
|
return val.asString();
|
||||||
|
}
|
||||||
|
if(val.isBoolean())
|
||||||
|
{
|
||||||
|
return val.asBoolean();
|
||||||
|
}
|
||||||
|
if(val.isNumber())
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// preserve integer types when possible //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
if(val.fitsInInt())
|
||||||
|
{
|
||||||
|
return val.asInt();
|
||||||
|
}
|
||||||
|
else if(val.fitsInLong())
|
||||||
|
{
|
||||||
|
return val.asLong();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new BigDecimal(val.asDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
// detect JS Date by existence of getTime() //
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
if(val.hasMember("getTime") && val.canInvokeMember("getTime"))
|
||||||
|
{
|
||||||
|
double millis = val.invokeMember("getTime").asDouble();
|
||||||
|
return Instant.ofEpochMilli((long) millis);
|
||||||
|
}
|
||||||
|
////////////
|
||||||
|
// arrays //
|
||||||
|
////////////
|
||||||
|
if(val.hasArrayElements())
|
||||||
|
{
|
||||||
|
List<Object> result = new ArrayList<>();
|
||||||
|
long size = val.getArraySize();
|
||||||
|
for(long i = 0; i < size; i++)
|
||||||
|
{
|
||||||
|
result.add(convertObjectToJava(val.getArrayElement(i)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/////////////
|
||||||
|
// objects //
|
||||||
|
/////////////
|
||||||
|
if(val.hasMembers())
|
||||||
|
{
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
for(String key : val.getMemberKeys())
|
||||||
|
{
|
||||||
|
result.put(key, convertObjectToJava(val.getMember(key)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
LOG.debug("Error unwrapping javascript date", e);
|
LOG.debug("Error converting GraalVM value", e);
|
||||||
}
|
|
||||||
|
|
||||||
if(scriptObjectMirror.isArray())
|
|
||||||
{
|
|
||||||
List<Object> 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<String, Object> result = new HashMap<>();
|
|
||||||
for(String key : scriptObjectMirror.keySet())
|
|
||||||
{
|
|
||||||
result.put(key, convertObjectToJava(scriptObjectMirror.get(key)));
|
|
||||||
}
|
|
||||||
return (result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return QCodeExecutor.super.convertObjectToJava(object);
|
return QCodeExecutor.super.convertObjectToJava(object);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
@ -165,9 +188,14 @@ public class QJavaScriptExecutor implements QCodeExecutor
|
|||||||
{
|
{
|
||||||
if(object instanceof Instant i)
|
if(object instanceof Instant i)
|
||||||
{
|
{
|
||||||
long millis = (i.getEpochSecond() * 1000 + i.getLong(ChronoField.MILLI_OF_SECOND));
|
long millis = (i.getEpochSecond() * 1000 + i.getLong(ChronoField.MILLI_OF_SECOND));
|
||||||
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
|
Context context = Context.newBuilder("js")
|
||||||
return engine.eval("new Date(" + millis + ")");
|
.allowAllAccess(true)
|
||||||
|
.allowExperimentalOptions(true)
|
||||||
|
.option("js.ecmascript-version", "2022")
|
||||||
|
.build();
|
||||||
|
Value jsDate = context.eval("js", "new Date(" + millis + ")");
|
||||||
|
return jsDate.asHostObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,32 +214,29 @@ public class QJavaScriptExecutor implements QCodeExecutor
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private Serializable runInline(String code, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
|
private Serializable runInline(String code, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
|
||||||
{
|
{
|
||||||
new NashornScriptEngineFactory();
|
Context context = Context.newBuilder("js")
|
||||||
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
|
.allowAllAccess(true)
|
||||||
|
.allowExperimentalOptions(true)
|
||||||
//////////////////////////////////////////////
|
.option("js.ecmascript-version", "2022")
|
||||||
// setup the javascript environment/context //
|
.build();
|
||||||
//////////////////////////////////////////////
|
// Populate GraalJS bindings from the inputContext
|
||||||
Bindings bindings = engine.createBindings();
|
Value bindingsScope = context.getBindings("js");
|
||||||
bindings.putAll(inputContext);
|
for(Map.Entry<String, Serializable> entry : inputContext.entrySet())
|
||||||
|
|
||||||
if(!bindings.containsKey("logger"))
|
|
||||||
{
|
{
|
||||||
bindings.put("logger", executionLogger);
|
bindingsScope.putMember(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
|
// Ensure logger is available
|
||||||
////////////////////////////////////////////////////////////////////////
|
if(!bindingsScope.hasMember("logger"))
|
||||||
// wrap the user's code in an immediately-invoked function expression //
|
{
|
||||||
// if the user's code (%s below) returns - then our IIFE is done. //
|
bindingsScope.putMember("logger", executionLogger);
|
||||||
// if the user's code doesn't return, but instead created a 'script' //
|
}
|
||||||
// variable, with a 'main' function on it (e.g., from a compiled //
|
// wrap the user's code in an immediately-invoked function expression
|
||||||
// type script file), then call main function and return its result. //
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
String codeToRun = """
|
String codeToRun = """
|
||||||
(function userDefinedFunction()
|
(function userDefinedFunction()
|
||||||
{
|
{
|
||||||
|
'use strict';
|
||||||
%s
|
%s
|
||||||
|
|
||||||
var mainFunction = null;
|
var mainFunction = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -221,7 +246,7 @@ public class QJavaScriptExecutor implements QCodeExecutor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e) { }
|
catch(e) { }
|
||||||
|
|
||||||
if(mainFunction != null)
|
if(mainFunction != null)
|
||||||
{
|
{
|
||||||
return (mainFunction());
|
return (mainFunction());
|
||||||
@ -232,66 +257,25 @@ public class QJavaScriptExecutor implements QCodeExecutor
|
|||||||
Serializable output;
|
Serializable output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = (Serializable) engine.eval(codeToRun, bindings);
|
Source source = Source.newBuilder("js", codeToRun, "batchName.js")
|
||||||
|
.mimeType("application/javascript+module")
|
||||||
|
.build();
|
||||||
|
Value result = context.eval(source);
|
||||||
|
output = (Serializable) result.asHostObject();
|
||||||
}
|
}
|
||||||
catch(ScriptException se)
|
catch(Exception se)
|
||||||
{
|
{
|
||||||
QCodeException qCodeException = getQCodeExceptionFromScriptException(se);
|
// We no longer have ScriptException, so wrap as QCodeException
|
||||||
throw (qCodeException);
|
throw new QCodeException("Error during JavaScript execution: " + se.getMessage(), se);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (output);
|
return (output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private QCodeException getQCodeExceptionFromScriptException(ScriptException se)
|
// getQCodeExceptionFromScriptException is now unused (ScriptException/Nashorn removed)
|
||||||
{
|
|
||||||
boolean isParserException = ExceptionUtils.findClassInRootChain(se, ParserException.class) != null;
|
|
||||||
boolean isUserThrownException = ExceptionUtils.findClassInRootChain(se, ECMAException.class) != null;
|
|
||||||
|
|
||||||
String message = se.getMessage();
|
|
||||||
String errorContext = null;
|
|
||||||
if(message != null)
|
|
||||||
{
|
|
||||||
message = message.replaceFirst(" in <eval>.*", "");
|
|
||||||
message = message.replaceFirst("<eval>:\\d+:\\d+", "");
|
|
||||||
|
|
||||||
if(message.contains("\n"))
|
|
||||||
{
|
|
||||||
String[] parts = message.split("\n", 2);
|
|
||||||
message = parts[0];
|
|
||||||
errorContext = parts[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int actualScriptLineNumber = se.getLineNumber() - 2;
|
|
||||||
|
|
||||||
String prefix = "Script Exception";
|
|
||||||
boolean includeColumn = true;
|
|
||||||
boolean includeContext = false;
|
|
||||||
if(isParserException)
|
|
||||||
{
|
|
||||||
prefix = "Script parser exception";
|
|
||||||
includeContext = true;
|
|
||||||
}
|
|
||||||
else if(isUserThrownException)
|
|
||||||
{
|
|
||||||
prefix = "Script threw an exception";
|
|
||||||
includeColumn = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QCodeException qCodeException = new QCodeException(prefix + " at line " + actualScriptLineNumber + (includeColumn ? (" column " + se.getColumnNumber()) : "") + ": " + message);
|
|
||||||
if(includeContext)
|
|
||||||
{
|
|
||||||
qCodeException.setContext(errorContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (qCodeException);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user