update to replace nashorn with graalvm javascript engine

This commit is contained in:
Tim Chamberlain
2025-05-08 16:30:36 -05:00
parent ce2ca3f413
commit 9ee2e4d4d7
2 changed files with 117 additions and 128 deletions

View File

@ -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 -->

View File

@ -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)
@ -166,8 +189,13 @@ 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,30 +214,27 @@ 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;
@ -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);
}