This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 04ed14caf2 add javascript to the script transform, fixes #6823 (#6835)
04ed14caf2 is described below
commit 04ed14caf250a121caf642c28ec778a713d0218f
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Mar 19 14:20:52 2026 +0100
add javascript to the script transform, fixes #6823 (#6835)
---
.../ROOT/pages/pipeline/transforms/script.adoc | 10 +-
plugins/transforms/script/pom.xml | 13 +
.../transforms/script/src/assembly/assembly.xml | 6 +
.../hop/pipeline/transforms/script/Script.java | 20 +
.../pipeline/transforms/script/ScriptUtils.java | 36 +-
.../script/ScriptAddedFunctionsTest.java | 48 +++
.../pipeline/transforms/script/ScriptDataTest.java | 38 ++
.../pipeline/transforms/script/ScriptMetaTest.java | 240 ++++++++++++
.../hop/pipeline/transforms/script/ScriptTest.java | 434 +++++++++++++++++++++
.../transforms/script/ScriptUtilsTest.java | 123 ++++++
.../hop/ui/core/widget/highlight/ScriptEngine.java | 90 +++++
11 files changed, 1046 insertions(+), 12 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
index fd9c09a934..ed72273540 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc
@@ -29,10 +29,11 @@ The Script transform allows you to write code in any
language supported by the h
By default, the Hop project ships with the following language support:
+* https://developer.mozilla.org/en-US/docs/Web/JavaScript[ECMAScript]
(JavaScript via https://www.graalvm.org/javascript/[GraalVM JavaScript], JSR
223)
* https://groovy-lang.org/[Groovy]
* https://www.python.org/[Python] (https://www.jython.org/[Jython])
-WARNING: ECMAScript (JavaScript as part of the JVM) was removed from the JVM
with Java 17. You can still add it in Apache Hop 2.10 and later as documented
in the <<_adding_scripting_languages, Adding Scripting Languages>> section.
+On Java 17 and later, the JVM no longer includes an ECMAScript engine; Hop
bundles GraalVM JavaScript so that the Script transform works without extra
setup.
|
@@ -252,9 +253,4 @@ For example, to add support for the Ruby scripting language
you need to add the
* `jruby-stdlib-<version>.jar`
* `jruby-core-<version>.jar`
-To add Javascript support, download the
https://repo1.maven.org/maven2/org/openjdk/nashorn/nashorn-core/[Nashorn Core
jar]
-
-* `nashorn-core-15.4.jar`
-
-
-After restarting the Apache Hop GUI you'll notice that there's a `ruby` or
`Javascript` entry in the `Scripting Engine` dropdown box.
+After restarting the Apache Hop GUI you'll notice that there's a `ruby` entry
in the `Scripting Engine` dropdown box.
diff --git a/plugins/transforms/script/pom.xml
b/plugins/transforms/script/pom.xml
index a71dfe6edf..28ad80330b 100644
--- a/plugins/transforms/script/pom.xml
+++ b/plugins/transforms/script/pom.xml
@@ -30,6 +30,7 @@
<name>Hop Plugins Transforms Script</name>
<properties>
+ <graaljs.version>23.0.2</graaljs.version>
<groovy.version>4.0.30</groovy.version>
<jython.version>2.7.4</jython.version>
</properties>
@@ -57,6 +58,12 @@
<artifactId>groovy-jsr223</artifactId>
<version>${groovy.version}</version>
</dependency>
+ <!-- GraalVM JavaScript (JSR 223) for ECMAScript on Java 17+ where
Nashorn was removed -->
+ <dependency>
+ <groupId>org.graalvm.js</groupId>
+ <artifactId>js-scriptengine</artifactId>
+ <version>${graaljs.version}</version>
+ </dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
@@ -84,5 +91,11 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.graalvm.js</groupId>
+ <artifactId>js</artifactId>
+ <version>${graaljs.version}</version>
+ <scope>runtime</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/plugins/transforms/script/src/assembly/assembly.xml
b/plugins/transforms/script/src/assembly/assembly.xml
index 01f59aadc2..424c5bd7e5 100644
--- a/plugins/transforms/script/src/assembly/assembly.xml
+++ b/plugins/transforms/script/src/assembly/assembly.xml
@@ -51,6 +51,12 @@
<include>org.apache.groovy:groovy:jar</include>
<include>org.apache.groovy:groovy-jsr223:jar</include>
<include>org.python:jython-standalone:jar</include>
+ <include>org.graalvm.js:js-scriptengine:jar</include>
+ <include>org.graalvm.js:js:jar</include>
+ <include>org.graalvm.regex:regex:jar</include>
+ <include>org.graalvm.truffle:truffle-api:jar</include>
+ <include>org.graalvm.sdk:graal-sdk:jar</include>
+ <include>com.ibm.icu:icu4j:jar</include>
</includes>
<outputDirectory>plugins/transforms/script/lib</outputDirectory>
</dependencySet>
diff --git
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
index 4089523e50..f1d1fa8906 100644
---
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
+++
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/Script.java
@@ -459,6 +459,10 @@ public class Script extends BaseTransform<ScriptMeta,
ScriptData> implements ITr
}
case IValueMeta.TYPE_INTEGER:
+ // Hop expects Long for TYPE_INTEGER; GraalVM JS can return
Integer or other Number.
+ if (result instanceof Number) {
+ return Long.valueOf(((Number) result).longValue());
+ }
if (classType.equalsIgnoreCase("java.lang.Byte")) {
return ((Byte) result).longValue();
} else if (classType.equalsIgnoreCase("java.lang.Short")) {
@@ -674,6 +678,22 @@ public class Script extends BaseTransform<ScriptMeta,
ScriptData> implements ITr
return bRC;
}
+ @Override
+ public void putRow(IRowMeta rowMeta, Object[] row) throws
HopTransformException {
+ // GraalVM JS can produce java.lang.Integer for numeric values; Hop's
TYPE_INTEGER
+ // expects Long (ValueMetaBase.getInteger does (Long) object). Normalize
so golden
+ // validation and sorting don't get ClassCastException.
+ if (rowMeta != null && row != null) {
+ for (int i = 0; i < rowMeta.size() && i < row.length; i++) {
+ if (rowMeta.getValueMeta(i).getType() == IValueMeta.TYPE_INTEGER
+ && row[i] instanceof Integer) {
+ row[i] = Long.valueOf(((Integer) row[i]).longValue());
+ }
+ }
+ }
+ super.putRow(rowMeta, row);
+ }
+
@Override
public boolean init() {
if (!super.init()) {
diff --git
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
index 095049be98..f709a8bd89 100644
---
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
+++
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptUtils.java
@@ -18,6 +18,7 @@
package org.apache.hop.pipeline.transforms.script;
+import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -26,9 +27,18 @@ import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import org.apache.hop.core.exception.HopException;
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.HostAccess;
public class ScriptUtils {
+ static {
+ // Suppress GraalVM "interpreted mode only" warning when running on a
standard JDK.
+ if (System.getProperty("polyglot.engine.WarnInterpreterOnly") == null) {
+ System.setProperty("polyglot.engine.WarnInterpreterOnly", "false");
+ }
+ }
+
private static ScriptEngineManager scriptEngineManager;
private static ScriptUtils instance;
@@ -61,8 +71,10 @@ public class ScriptUtils {
* @return the desired ScriptEngine, or null if none can be found
*/
private static ScriptEngine createNewScriptEngine(String engineName) {
-
ScriptEngine scriptEngine =
getScriptEngineManager().getEngineByName(engineName);
+ if (scriptEngine instanceof GraalJSScriptEngine) {
+ return createGraalJsEngine();
+ }
if (scriptEngine == null) {
// falls back to Groovy
scriptEngine = scriptEngineManager.getEngineByName("groovy");
@@ -105,8 +117,6 @@ public class ScriptUtils {
System.setProperty(
"org.jruby.embed.localvariable.behavior",
"persistent"); // required for JRuby, transparent
- System.setProperty("nashorn.args", "--language=es6");
- // for others
scriptEngineManager = new
ScriptEngineManager(ScriptUtils.class.getClassLoader());
}
return scriptEngineManager;
@@ -136,7 +146,23 @@ public class ScriptUtils {
}
}
- public ScriptEngine getScriptEngineByName(String scriptLanguegeName) {
- return scriptEngineManager.getEngineByName(scriptLanguegeName);
+ public ScriptEngine getScriptEngineByName(String scriptLanguageName) {
+ ScriptEngine engine =
scriptEngineManager.getEngineByName(scriptLanguageName);
+ // GraalVM JS runs in a restricted sandbox by default when obtained through
+ // ScriptEngineManager. Re-create it with HostAccess.ALL so scripts can
call
+ // methods on bound Java objects (e.g. transform.logBasic(...)), matching
the
+ // behaviour of the Nashorn engine that was removed in Java 17.
+ if (engine instanceof GraalJSScriptEngine) {
+ return createGraalJsEngine();
+ }
+ return engine;
+ }
+
+ private static ScriptEngine createGraalJsEngine() {
+ return GraalJSScriptEngine.create(
+ null,
+ Context.newBuilder("js")
+ .allowHostAccess(HostAccess.ALL)
+ .allowHostClassLookup(className -> true));
}
}
diff --git
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptAddedFunctionsTest.java
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptAddedFunctionsTest.java
new file mode 100644
index 0000000000..46a14831a8
--- /dev/null
+++
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptAddedFunctionsTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class ScriptAddedFunctionsTest {
+
+ @Test
+ void undefinedValue_isNull() {
+ assertNull(ScriptAddedFunctions.undefinedValue);
+ }
+
+ @Test
+ void functionTypeConstants_haveExpectedValues() {
+ assertEquals(0, ScriptAddedFunctions.STRING_FUNCTION);
+ assertEquals(1, ScriptAddedFunctions.NUMERIC_FUNCTION);
+ assertEquals(2, ScriptAddedFunctions.DATE_FUNCTION);
+ assertEquals(3, ScriptAddedFunctions.LOGIC_FUNCTION);
+ assertEquals(4, ScriptAddedFunctions.SPECIAL_FUNCTION);
+ assertEquals(5, ScriptAddedFunctions.FILE_FUNCTION);
+ }
+
+ @Test
+ void jsFunctionList_isEmptyByDefault() {
+ assertNotNull(ScriptAddedFunctions.jsFunctionList);
+ assertEquals(0, ScriptAddedFunctions.jsFunctionList.length);
+ }
+}
diff --git
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptDataTest.java
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptDataTest.java
new file mode 100644
index 0000000000..612dade1aa
--- /dev/null
+++
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptDataTest.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.script;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class ScriptDataTest {
+
+ @Test
+ void defaultConstructor_setsNullFields() {
+ ScriptData data = new ScriptData();
+ assertNull(data.engine);
+ assertNull(data.context);
+ assertNull(data.compiledScript);
+ assertNull(data.rawScript);
+ assertNull(data.fieldsUsed);
+ assertNull(data.valuesUsed);
+ assertNull(data.outputRowMeta);
+ assertNull(data.replaceIndex);
+ }
+}
diff --git
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptMetaTest.java
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptMetaTest.java
new file mode 100644
index 0000000000..624d1f900e
--- /dev/null
+++
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptMetaTest.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class ScriptMetaTest {
+
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ private ScriptMeta meta;
+ private IRowMeta rowMeta;
+ private IVariables variables;
+ private IHopMetadataProvider metadataProvider;
+
+ @BeforeEach
+ void setUp() {
+ meta = new ScriptMeta();
+ rowMeta = new org.apache.hop.core.row.RowMeta();
+ variables = mock(IVariables.class);
+ metadataProvider = mock(IHopMetadataProvider.class);
+ }
+
+ @Test
+ void getLanguageName_defaultIsNull() {
+ assertNull(meta.getLanguageName());
+ }
+
+ @Test
+ void setLanguageName_getLanguageName_roundTrip() {
+ meta.setLanguageName("Groovy");
+ assertEquals("Groovy", meta.getLanguageName());
+ meta.setLanguageName("ECMAScript");
+ assertEquals("ECMAScript", meta.getLanguageName());
+ }
+
+ @Test
+ void getScripts_defaultIsEmptyList() {
+ assertNotNull(meta.getScripts());
+ assertTrue(meta.getScripts().isEmpty());
+ }
+
+ @Test
+ void setScripts_getScripts_roundTrip() {
+ List<ScriptMeta.SScript> scripts = new ArrayList<>();
+ ScriptMeta.SScript s = new ScriptMeta.SScript();
+ s.setScriptName("s1");
+ s.setScript("x=1");
+ scripts.add(s);
+ meta.setScripts(scripts);
+ assertEquals(1, meta.getScripts().size());
+ assertEquals("s1", meta.getScripts().get(0).getScriptName());
+ assertEquals("x=1", meta.getScripts().get(0).getScript());
+ }
+
+ @Test
+ void getFields_defaultIsEmptyList() {
+ assertNotNull(meta.getFields());
+ assertTrue(meta.getFields().isEmpty());
+ }
+
+ @Test
+ void setFields_getFields_roundTrip() {
+ List<ScriptMeta.SField> fields = new ArrayList<>();
+ ScriptMeta.SField f = new ScriptMeta.SField();
+ f.setName("out1");
+ f.setType("String");
+ fields.add(f);
+ meta.setFields(fields);
+ assertEquals(1, meta.getFields().size());
+ assertEquals("out1", meta.getFields().get(0).getName());
+ assertEquals("String", meta.getFields().get(0).getType());
+ }
+
+ @Test
+ void clone_returnsNewInstanceWithSameContent() {
+ meta.setLanguageName("Groovy");
+ meta.getScripts()
+ .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT,
"s1", "1+1"));
+ meta.getFields().add(createField("f1", "Integer"));
+
+ ScriptMeta cloned = meta.clone();
+ assertNotNull(cloned);
+ assertFalse(cloned == meta);
+ assertEquals("Groovy", cloned.getLanguageName());
+ assertEquals(1, cloned.getScripts().size());
+ assertEquals("s1", cloned.getScripts().get(0).getScriptName());
+ assertEquals(1, cloned.getFields().size());
+ assertEquals("f1", cloned.getFields().get(0).getName());
+ }
+
+ private static ScriptMeta.SField createField(String name, String type) {
+ ScriptMeta.SField f = new ScriptMeta.SField();
+ f.setName(name);
+ f.setType(type);
+ return f;
+ }
+
+ @Test
+ void getFields_addsNewFieldWhenNotReplace() throws HopTransformException {
+ ScriptMeta.SField field = createField("newField", "String");
+ field.setRename("newField"); // when rename is set, getFields uses
getName() for the new field
+ meta.getFields().add(field);
+ meta.getFields(rowMeta, "Script", null, null, variables, metadataProvider);
+ assertEquals(1, rowMeta.size());
+ assertEquals("newField", rowMeta.getValueMeta(0).getName());
+ }
+
+ @Test
+ void getFields_skipsFieldWithEmptyName() throws HopTransformException {
+ meta.getFields().add(createField("", "String"));
+ ScriptMeta.SField valid = createField("valid", "Integer");
+ valid.setRename("valid");
+ meta.getFields().add(valid);
+ meta.getFields(rowMeta, "Script", null, null, variables, metadataProvider);
+ assertEquals(1, rowMeta.size());
+ assertEquals("valid", rowMeta.getValueMeta(0).getName());
+ }
+
+ @Test
+ void getFields_replacesExistingFieldWhenReplaceTrue() throws
HopTransformException {
+ rowMeta.addValueMeta(new ValueMetaString("existing"));
+ ScriptMeta.SField field = createField("existing", "Integer");
+ field.setReplace(true);
+ field.setRename("existing");
+ meta.getFields().add(field);
+ meta.getFields(rowMeta, "Script", null, null, variables, metadataProvider);
+ assertEquals(1, rowMeta.size());
+ assertEquals("existing", rowMeta.getValueMeta(0).getName());
+ assertEquals(
+ org.apache.hop.core.row.IValueMeta.TYPE_INTEGER,
rowMeta.getValueMeta(0).getType());
+ }
+
+ @Test
+ void getFields_throwsWhenReplaceTrueAndFieldNotFound() {
+ ScriptMeta.SField field = createField("missing", "String");
+ field.setReplace(true);
+ field.setRename("");
+ meta.getFields().add(field);
+ assertThrows(
+ HopTransformException.class,
+ () -> meta.getFields(rowMeta, "Script", null, null, variables,
metadataProvider));
+ }
+
+ @Test
+ void sField_getHopType_returnsCorrectType() {
+ ScriptMeta.SField f = new ScriptMeta.SField();
+ f.setType("String");
+ assertEquals(IValueMeta.TYPE_STRING, f.getHopType());
+ f.setType("Integer");
+ assertEquals(IValueMeta.TYPE_INTEGER, f.getHopType());
+ }
+
+ @Test
+ void sField_copyConstructor() {
+ ScriptMeta.SField orig = new ScriptMeta.SField();
+ orig.setName("a");
+ orig.setRename("b");
+ orig.setType("Number");
+ orig.setLength(10);
+ orig.setPrecision(2);
+ orig.setReplace(true);
+ orig.setScriptResult(true);
+ ScriptMeta.SField copy = new ScriptMeta.SField(orig);
+ assertEquals("a", copy.getName());
+ assertEquals("b", copy.getRename());
+ assertEquals("Number", copy.getType());
+ assertEquals(10, copy.getLength());
+ assertEquals(2, copy.getPrecision());
+ assertTrue(copy.isReplace());
+ assertTrue(copy.isScriptResult());
+ }
+
+ @Test
+ void sScript_scriptTypeHelpers() {
+ ScriptMeta.SScript s = new ScriptMeta.SScript();
+ s.setScriptType(ScriptMeta.ScriptType.TRANSFORM_SCRIPT);
+ assertTrue(s.isTransformScript());
+ assertFalse(s.isStartScript());
+ assertFalse(s.isEndScript());
+ s.setScriptType(ScriptMeta.ScriptType.START_SCRIPT);
+ assertTrue(s.isStartScript());
+ s.setScriptType(ScriptMeta.ScriptType.END_SCRIPT);
+ assertTrue(s.isEndScript());
+ }
+
+ @Test
+ void sScript_copyConstructor() {
+ ScriptMeta.SScript orig =
+ new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, "sn",
"body");
+ ScriptMeta.SScript copy = new ScriptMeta.SScript(orig);
+ assertEquals(ScriptMeta.ScriptType.TRANSFORM_SCRIPT, copy.getScriptType());
+ assertEquals("sn", copy.getScriptName());
+ assertEquals("body", copy.getScript());
+ }
+
+ @Test
+ void scriptType_enumValues() {
+ assertEquals("-1", ScriptMeta.ScriptType.NORMAL_SCRIPT.getCode());
+ assertEquals("0", ScriptMeta.ScriptType.TRANSFORM_SCRIPT.getCode());
+ assertEquals("1", ScriptMeta.ScriptType.START_SCRIPT.getCode());
+ assertEquals("2", ScriptMeta.ScriptType.END_SCRIPT.getCode());
+ assertNotNull(ScriptMeta.ScriptType.TRANSFORM_SCRIPT.getDescription());
+ }
+}
diff --git
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptTest.java
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptTest.java
new file mode 100644
index 0000000000..c54200a5fe
--- /dev/null
+++
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptTest.java
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaPluginType;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class ScriptTest {
+
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ private TransformMockHelper<ScriptMeta, ScriptData> transformMockHelper;
+ private Script script;
+ private ScriptMeta meta;
+
+ @BeforeAll
+ static void initPlugins() throws HopException {
+ ValueMetaPluginType.getInstance().searchPlugins();
+ }
+
+ @BeforeEach
+ void setUp() {
+ transformMockHelper =
+ new TransformMockHelper<>("Script TEST", ScriptMeta.class,
ScriptData.class);
+ when(transformMockHelper.logChannelFactory.create(any(),
any(ILoggingObject.class)))
+ .thenReturn(transformMockHelper.iLogChannel);
+ when(transformMockHelper.pipeline.isRunning()).thenReturn(true);
+
+ meta = new ScriptMeta();
+
when(transformMockHelper.iTransformMeta.getFields()).thenReturn(meta.getFields());
+
when(transformMockHelper.iTransformMeta.getScripts()).thenReturn(meta.getScripts());
+
when(transformMockHelper.iTransformMeta.getLanguageName()).thenReturn("Groovy");
+
+ script =
+ new Script(
+ transformMockHelper.transformMeta,
+ meta,
+ transformMockHelper.iTransformData,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ }
+
+ @AfterEach
+ void tearDown() {
+ transformMockHelper.cleanUp();
+ }
+
+ // --- getValueFromScript: TYPE_INTEGER ---
+
+ @Test
+ void getValueFromScript_integer_returnsLong() throws HopValueException {
+ meta.getFields().add(createField("id", "Integer"));
+ assertEquals(42L, script.getValueFromScript(42, 0));
+ assertEquals(42L, script.getValueFromScript(Integer.valueOf(42), 0));
+ }
+
+ @Test
+ void getValueFromScript_long_returnsLong() throws HopValueException {
+ meta.getFields().add(createField("id", "Integer"));
+ assertEquals(42L, script.getValueFromScript(42L, 0));
+ }
+
+ @Test
+ void getValueFromScript_numberDouble_returnsLong() throws HopValueException {
+ meta.getFields().add(createField("id", "Integer"));
+ assertEquals(3L, script.getValueFromScript(3.7, 0));
+ }
+
+ @Test
+ void getValueFromScript_string_returnsLong() throws HopValueException {
+ meta.getFields().add(createField("id", "Integer"));
+ assertEquals(100L, script.getValueFromScript("100", 0));
+ }
+
+ @Test
+ void getValueFromScript_null_returnsNull() throws HopValueException {
+ meta.getFields().add(createField("id", "Integer"));
+ assertNull(script.getValueFromScript(null, 0));
+ }
+
+ @Test
+ void getValueFromScript_emptyName_throws() {
+ ScriptMeta.SField field = createField("", "Integer");
+ meta.getFields().add(field);
+ assertThrows(HopValueException.class, () -> script.getValueFromScript(1,
0));
+ }
+
+ // --- getValueFromScript: TYPE_NUMBER ---
+
+ @Test
+ void getValueFromScript_numberType_integer_returnsDouble() throws
HopValueException {
+ meta.getFields().add(createField("x", "Number"));
+ assertEquals(42.0, script.getValueFromScript(42, 0));
+ }
+
+ @Test
+ void getValueFromScript_numberType_double_returnsDouble() throws
HopValueException {
+ meta.getFields().add(createField("x", "Number"));
+ assertEquals(3.14, (Double) script.getValueFromScript(3.14, 0), 1e-9);
+ }
+
+ @Test
+ void getValueFromScript_numberType_null_returnsNull() throws
HopValueException {
+ meta.getFields().add(createField("x", "Number"));
+ assertNull(script.getValueFromScript(null, 0));
+ }
+
+ // --- getValueFromScript: TYPE_STRING ---
+
+ @Test
+ void getValueFromScript_stringType_returnsString() throws HopValueException {
+ meta.getFields().add(createField("name", "String"));
+ assertEquals("hello", script.getValueFromScript("hello", 0));
+ }
+
+ // --- getValueFromScript: TYPE_BOOLEAN ---
+
+ @Test
+ void getValueFromScript_booleanType_returnsBoolean() throws
HopValueException {
+ meta.getFields().add(createField("flag", "Boolean"));
+ assertEquals(Boolean.TRUE, script.getValueFromScript(true, 0));
+ }
+
+ // --- getValueFromScript: TYPE_DATE ---
+
+ @Test
+ void getValueFromScript_dateType_date_returnsDate() throws HopValueException
{
+ meta.getFields().add(createField("d", "Date"));
+ Date d = new Date(1234567890L);
+ assertEquals(d, script.getValueFromScript(d, 0));
+ }
+
+ @Test
+ void getValueFromScript_dateType_null_returnsNull() throws HopValueException
{
+ meta.getFields().add(createField("d", "Date"));
+ assertNull(script.getValueFromScript(null, 0));
+ }
+
+ // --- getValueFromScript: TYPE_BIGNUMBER ---
+
+ @Test
+ void getValueFromScript_bignumberType_integer_returnsBigDecimal() throws
HopValueException {
+ meta.getFields().add(createField("bn", "BigNumber"));
+ assertEquals(new BigDecimal("42"), script.getValueFromScript(42, 0));
+ }
+
+ @Test
+ void getValueFromScript_bignumberType_bigDecimal_returnsBigDecimal() throws
HopValueException {
+ meta.getFields().add(createField("bn", "BigNumber"));
+ // Script.getValueFromScript TYPE_BIGNUMBER handles NativeNumber,
NativeJavaObject, boxed
+ // primitives, String;
+ // java.math.BigDecimal falls through to default and throws (could be
extended to handle
+ // BigDecimal).
+ BigDecimal bd = new BigDecimal("123.45");
+ assertThrows(HopValueException.class, () -> script.getValueFromScript(bd,
0));
+ }
+
+ // --- putRow: Integer normalized to Long ---
+
+ @Test
+ void putRow_normalizesIntegerToLongForIntegerColumn() throws Exception {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaInteger("id"));
+ rowMeta.addValueMeta(new ValueMetaString("name"));
+
+ Object[] row = new Object[] {Integer.valueOf(99), "test"};
+
+ IRowSet outputSet = mock(IRowSet.class, Mockito.RETURNS_MOCKS);
+ when(outputSet.putRow(any(IRowMeta.class),
any(Object[].class))).thenReturn(true);
+ when(outputSet.isDone()).thenReturn(false);
+ when(outputSet.getRowWait(anyLong(),
any(TimeUnit.class))).thenReturn(null);
+
+ script.addRowSetToOutputRowSets(outputSet);
+ script.putRow(rowMeta, row);
+
+ ArgumentCaptor<Object[]> rowCaptor =
ArgumentCaptor.forClass(Object[].class);
+ verify(outputSet).putRow(any(IRowMeta.class), rowCaptor.capture());
+ Object[] captured = rowCaptor.getValue();
+ assertInstanceOf(Long.class, captured[0], "Integer column should be
normalized to Long");
+ assertEquals(99L, captured[0]);
+ assertEquals("test", captured[1]);
+ }
+
+ @Test
+ void putRow_doesNotModifyRowWhenNoIntegerColumn() throws Exception {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("name"));
+
+ Object[] row = new Object[] {"hello"};
+
+ IRowSet outputSet = mock(IRowSet.class, Mockito.RETURNS_MOCKS);
+ when(outputSet.putRow(any(IRowMeta.class),
any(Object[].class))).thenReturn(true);
+ when(outputSet.isDone()).thenReturn(false);
+
+ script.addRowSetToOutputRowSets(outputSet);
+ script.putRow(rowMeta, row);
+
+ assertEquals("hello", row[0]);
+ }
+
+ @Test
+ void putRow_handlesNullRowMeta() throws Exception {
+ script.putRow(null, new Object[] {1});
+ // no exception when rowMeta is null (normalization loop skipped)
+ }
+
+ @Test
+ void getValueFromScript_indexOutOfBounds_throws() {
+ meta.getFields().add(createField("a", "Integer"));
+ assertThrows(IndexOutOfBoundsException.class, () ->
script.getValueFromScript(1, 1));
+ }
+
+ // --- getValueFromScript: more branches ---
+
+ @Test
+ void getValueFromScript_numberType_parseFromToString() throws
HopValueException {
+ meta.getFields().add(createField("x", "Number"));
+ Object obj =
+ new Object() {
+ @Override
+ public String toString() {
+ return "2.5";
+ }
+ };
+ assertEquals(2.5, (Double) script.getValueFromScript(obj, 0), 1e-9);
+ }
+
+ @Test
+ void getValueFromScript_integer_invalidString_throws() {
+ meta.getFields().add(createField("id", "Integer"));
+ assertThrows(HopValueException.class, () ->
script.getValueFromScript("not-a-number", 0));
+ }
+
+ @Test
+ void getValueFromScript_stringType_nonStringObject_returnsResult() throws
HopValueException {
+ meta.getFields().add(createField("name", "String"));
+ Integer value = 42;
+ assertEquals(value, script.getValueFromScript(value, 0));
+ }
+
+ @Test
+ void getValueFromScript_dateType_doubleTimestamp_returnsDate() throws
HopValueException {
+ meta.getFields().add(createField("d", "Date"));
+ long ts = 1234567890L;
+ assertEquals(new Date(ts), script.getValueFromScript((double) ts, 0));
+ }
+
+ @Test
+ void getValueFromScript_dateType_numericString_returnsDate() throws
HopValueException {
+ meta.getFields().add(createField("d", "Date"));
+ long ts = 1000000L;
+ Date expected = new Date(ts);
+ assertEquals(expected, script.getValueFromScript(String.valueOf(ts), 0));
+ }
+
+ @Test
+ void getValueFromScript_bignumberType_string_returnsBigDecimal() throws
HopValueException {
+ meta.getFields().add(createField("bn", "BigNumber"));
+ assertEquals(new BigDecimal("999"), script.getValueFromScript("999", 0));
+ }
+
+ @Test
+ void getValueFromScript_binaryType_returnsResult() throws HopValueException {
+ meta.getFields().add(createField("bin", "Binary"));
+ byte[] bytes = new byte[] {1, 2, 3};
+ assertEquals(bytes, script.getValueFromScript(bytes, 0));
+ }
+
+ @Test
+ void getValueFromScript_typeNone_throws() {
+ ScriptMeta.SField field = createField("x", "NoSuchType");
+ meta.getFields().add(field);
+ assertThrows(HopValueException.class, () -> script.getValueFromScript(1,
0));
+ }
+
+ @Test
+ void init_setsEngineWhenLanguageAvailable() throws Exception {
+ ScriptData data = new ScriptData();
+ meta.setLanguageName("Groovy");
+ Script scriptWithData =
+ new Script(
+ transformMockHelper.transformMeta,
+ meta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ assertTrue(scriptWithData.init());
+ assertNotNull(data.engine);
+ }
+
+ @Test
+ void init_stillReturnsTrueWhenEngineLookupFails() throws Exception {
+ ScriptData data = new ScriptData();
+ meta.setLanguageName("NonExistentLanguageXYZ");
+ Script scriptWithData =
+ new Script(
+ transformMockHelper.transformMeta,
+ meta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ assertTrue(scriptWithData.init());
+ assertNull(data.engine);
+ }
+
+ @Test
+ void init_copiesScriptsFromMeta() throws Exception {
+ ScriptData data = new ScriptData();
+ meta.setLanguageName("Groovy");
+ meta.getScripts()
+ .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT,
"s1", "x=1"));
+ meta.getScripts()
+ .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.START_SCRIPT,
"start", "y=0"));
+ meta.getScripts().add(new
ScriptMeta.SScript(ScriptMeta.ScriptType.END_SCRIPT, "end", "z=2"));
+ Script scriptWithData =
+ new Script(
+ transformMockHelper.transformMeta,
+ meta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ assertTrue(scriptWithData.init());
+ assertNotNull(data.engine);
+ }
+
+ @Test
+ void putRow_nullRow_doesNotThrow() throws Exception {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaInteger("id"));
+ script.putRow(rowMeta, null);
+ }
+
+ /** Runs one row through the transform: addValues, eval script, putRow. */
+ @Test
+ void processRow_oneRow_producesOutputRow() throws Exception {
+ ScriptData data = new ScriptData();
+ meta.setLanguageName("Groovy");
+ meta.getScripts()
+ .add(new ScriptMeta.SScript(ScriptMeta.ScriptType.TRANSFORM_SCRIPT,
"s1", "out=1"));
+ ScriptMeta.SField outField = createField("out", "Integer");
+ outField.setRename("out");
+ meta.getFields().add(outField);
+
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("dummy"));
+ Object[] inputRow = new Object[] {"x"};
+
+ Script runScript =
+ new Script(
+ transformMockHelper.transformMeta,
+ meta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ runScript.addRowSetToInputRowSets(
+
transformMockHelper.getMockInputRowSet(TransformMockHelper.asList(inputRow)));
+ IRowSet outputSet = mock(IRowSet.class, Mockito.RETURNS_MOCKS);
+ when(outputSet.putRow(any(IRowMeta.class),
any(Object[].class))).thenReturn(true);
+ when(outputSet.isDone()).thenReturn(false);
+ when(outputSet.getRowWait(anyLong(),
any(TimeUnit.class))).thenReturn(null);
+ runScript.addRowSetToOutputRowSets(outputSet);
+
+ assertTrue(runScript.init());
+ assertTrue(runScript.processRow());
+ runScript.processRow();
+
+ ArgumentCaptor<Object[]> rowCaptor =
ArgumentCaptor.forClass(Object[].class);
+ ArgumentCaptor<IRowMeta> metaCaptor =
ArgumentCaptor.forClass(IRowMeta.class);
+ verify(outputSet).putRow(metaCaptor.capture(), rowCaptor.capture());
+ Object[] outRow = rowCaptor.getValue();
+ assertNotNull(outRow);
+ assertEquals(1, outRow.length);
+ assertEquals(1L, outRow[0]);
+ }
+
+ private static ScriptMeta.SField createField(String name, String type) {
+ ScriptMeta.SField f = new ScriptMeta.SField();
+ f.setName(name);
+ f.setType(type);
+ return f;
+ }
+}
diff --git
a/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptUtilsTest.java
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptUtilsTest.java
new file mode 100644
index 0000000000..59450c433e
--- /dev/null
+++
b/plugins/transforms/script/src/test/java/org/apache/hop/pipeline/transforms/script/ScriptUtilsTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.script;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import org.junit.jupiter.api.Test;
+
+class ScriptUtilsTest {
+
+ @Test
+ void getInstance_returnsSingleton() {
+ ScriptUtils a = ScriptUtils.getInstance();
+ ScriptUtils b = ScriptUtils.getInstance();
+ assertNotNull(a);
+ assertSame(a, b);
+ }
+
+ @Test
+ void getScriptLanguageNames_returnsNonEmptyList() {
+ assertNotNull(ScriptUtils.getInstance().getScriptLanguageNames());
+ assertTrue(ScriptUtils.getInstance().getScriptLanguageNames().size() > 0);
+ }
+
+ @Test
+ void getScriptLanguageNames_containsGroovy() {
+ assertTrue(
+ ScriptUtils.getInstance().getScriptLanguageNames().contains("Groovy"),
+ "Expected Groovy in script language names");
+ }
+
+ @Test
+ void getScriptLanguageNames_containsECMAScript() {
+ assertTrue(
+
ScriptUtils.getInstance().getScriptLanguageNames().contains("ECMAScript"),
+ "Expected ECMAScript in script language names (GraalVM JS)");
+ }
+
+ @Test
+ void getScriptEngineByName_groovy_returnsEngine() {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("Groovy");
+ assertNotNull(engine);
+ }
+
+ @Test
+ void getScriptEngineByName_groovy_evalSucceeds() throws ScriptException {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("Groovy");
+ assertNotNull(engine);
+ Object result = engine.eval("1 + 2");
+ assertEquals(3, result);
+ }
+
+ @Test
+ void getScriptEngineByName_ecmascript_returnsEngine() {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("ECMAScript");
+ assertNotNull(engine);
+ }
+
+ @Test
+ void getScriptEngineByName_ecmascript_evalSucceeds() throws ScriptException {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("ECMAScript");
+ assertNotNull(engine);
+ Object result = engine.eval("1 + 2");
+ assertNotNull(result);
+ assertTrue(result instanceof Number, "Expected number, got " +
result.getClass().getName());
+ assertEquals(3, ((Number) result).intValue());
+ }
+
+ @Test
+ void getScriptEngineByName_ecmascript_returnsGraalEngineWithHostAccess()
throws ScriptException {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("ECMAScript");
+ assertNotNull(engine);
+ assertTrue(
+ engine instanceof GraalJSScriptEngine, "ECMAScript engine should be
GraalJSScriptEngine");
+
+ // Verify host access: script can call a method on a bound Java object
(e.g. transform.logBasic)
+ engine.put("holder", new StringBuilder());
+ engine.eval("holder.append('ok')");
+ assertEquals("ok", engine.get("holder").toString());
+ }
+
+ @Test
+ void getScriptEngineByName_unknown_returnsNull() {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("NonExistentLanguageXYZ");
+ assertNull(engine);
+ }
+
+ @Test
+ void getScriptEngineByName_python_returnsEngine() {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("python");
+ assertNotNull(engine);
+ }
+
+ @Test
+ void getScriptEngineByName_python_evalSucceeds() throws ScriptException {
+ ScriptEngine engine =
ScriptUtils.getInstance().getScriptEngineByName("python");
+ assertNotNull(engine);
+ Object result = engine.eval("1 + 2");
+ assertEquals(3, result);
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
index c13fcca223..42674e58a7 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/highlight/ScriptEngine.java
@@ -127,6 +127,96 @@ public enum ScriptEngine {
"vars",
"zip",
"__import__")),
+ ECMASCRIPT(
+ List.of(
+ "async",
+ "await",
+ "break",
+ "case",
+ "catch",
+ "class",
+ "const",
+ "continue",
+ "debugger",
+ "default",
+ "delete",
+ "do",
+ "else",
+ "export",
+ "extends",
+ "false",
+ "finally",
+ "for",
+ "function",
+ "if",
+ "import",
+ "in",
+ "instanceof",
+ "let",
+ "new",
+ "null",
+ "return",
+ "super",
+ "switch",
+ "this",
+ "throw",
+ "true",
+ "try",
+ "typeof",
+ "var",
+ "void",
+ "while",
+ "yield"),
+ List.of(
+ "Array",
+ "ArrayBuffer",
+ "BigInt",
+ "Boolean",
+ "DataView",
+ "Date",
+ "Error",
+ "EvalError",
+ "Float32Array",
+ "Float64Array",
+ "Function",
+ "Infinity",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Intl",
+ "JSON",
+ "Map",
+ "Math",
+ "NaN",
+ "Number",
+ "Object",
+ "Promise",
+ "Proxy",
+ "RangeError",
+ "ReferenceError",
+ "Reflect",
+ "RegExp",
+ "Set",
+ "String",
+ "Symbol",
+ "SyntaxError",
+ "TypeError",
+ "Uint8Array",
+ "Uint16Array",
+ "Uint32Array",
+ "URIError",
+ "WeakMap",
+ "WeakSet",
+ "decodeURI",
+ "decodeURIComponent",
+ "encodeURI",
+ "encodeURIComponent",
+ "eval",
+ "isFinite",
+ "isNaN",
+ "parseFloat",
+ "parseInt",
+ "undefined")),
GROOVY(
List.of(
"as",