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 155346e5da code hardening script transform, fixes #4944 (#7838)
155346e5da is described below

commit 155346e5da79c5085fb0b1df8321aa79f6e57174
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Aug 7 22:31:00 2026 +0200

    code hardening script transform, fixes #4944 (#7838)
---
 .../transforms/javascript/ScriptValues.java        |  66 ++++++----
 .../transforms/javascript/ScriptValuesDialog.java  |  41 ++++--
 .../transforms/javascript/ScriptValuesMeta.java    |  20 ++-
 .../transforms/javascript/jsFunctionHelp.xml       |  21 ++-
 .../javascript/messages/messages_en_US.properties  |   9 ++
 .../javascript/ScriptValuesMetaTest.java           |  48 +++++++
 .../transforms/javascript/ScriptValuesTest.java    | 143 ++++++++++++++++++++-
 7 files changed, 303 insertions(+), 45 deletions(-)

diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
index c9674a3204..f658538ac7 100644
--- 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValues.java
@@ -41,6 +41,7 @@ import org.mozilla.javascript.EvaluatorException;
 import org.mozilla.javascript.Script;
 import org.mozilla.javascript.Scriptable;
 import org.mozilla.javascript.ScriptableObject;
+import org.mozilla.javascript.Undefined;
 
 /**
  * Executes a JavaScript on the values in the input stream. Selected 
calculated values can then be
@@ -97,27 +98,13 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
   }
 
   private void determineUsedFields(IRowMeta row) {
-    int nr = 0;
-    // Count the occurrences of the values.
+    // Collect the fields the script mentions. JavaScript identifiers are case 
sensitive and
+    // addFieldsToScope() binds each field under its exact name, so the match 
has to be case
+    // sensitive too -- matching case insensitively here only creates unusable 
slots.
     // Perhaps we find values in comments, but we take no risk!
     //
+    List<Integer> usedFields = new ArrayList<>();
     for (int i = 0; i < row.size(); i++) {
-      String valueName = row.getValueMeta(i).getName().toUpperCase();
-      if (strTransformScript.toUpperCase().contains(valueName)) {
-        nr++;
-      }
-    }
-
-    // Allocate fields_used
-    data.fieldsUsed = new int[nr];
-
-    nr = 0;
-    // Count the occurrences of the values.
-    // Perhaps we find values in comments, but we take no risk!
-    //
-    for (int i = 0; i < row.size(); i++) {
-      // Values are case-insensitive in JavaScript.
-      //
       String valname = row.getValueMeta(i).getName();
       if (strTransformScript.contains(valname)) {
         if (isDetailed()) {
@@ -125,11 +112,12 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
               BaseMessages.getString(
                   PKG, "ScriptValuesMod.Log.UsedValueName", String.valueOf(i), 
valname));
         }
-        data.fieldsUsed[nr] = i;
-        nr++;
+        usedFields.add(i);
       }
     }
 
+    data.fieldsUsed = usedFields.stream().mapToInt(i -> i).toArray();
+
     if (isDetailed()) {
       logDetailed(
           BaseMessages.getString(
@@ -296,12 +284,11 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
         if (pipelineStatus != Scriptable.NOT_FOUND) {
           bWithPipelineStat = true;
           if (isDetailed()) {
-            logDetailed(
-                ("pipeline_Status found. Checking pipeline status while script 
execution."));
+            logDetailed(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.PipelineStatusUsed"));
           }
         } else {
           if (isDetailed()) {
-            logDetailed(("No pipeline_Status found. Pipeline status checking 
not available."));
+            logDetailed(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.PipelineStatusNotUsed"));
           }
           bWithPipelineStat = false;
         }
@@ -374,11 +361,11 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
         Script startScript = data.context.compileString(strStartScript, 
"pipeline_Start", 1, null);
         startScript.exec(data.context, data.scope, data.scope);
         if (isDetailed()) {
-          logDetailed(("Start Script found!"));
+          logDetailed(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.StartScriptExecuted"));
         }
       } else {
         if (isDetailed()) {
-          logDetailed(("No starting Script found!"));
+          logDetailed(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.NoStartScriptDefined"));
         }
       }
     } catch (Exception es) {
@@ -433,6 +420,17 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
     ScriptValuesMeta.ScriptField field = meta.getScriptFields().get(i);
     if (!Utils.isEmpty(field.getName())) {
       try {
+        // The script never assigned this field: Rhino hands back its 
NOT_FOUND sentinel, which
+        // would otherwise be stringified into the row as "...UniqueTag@1234: 
NOT_FOUND".
+        //
+        if (result == Scriptable.NOT_FOUND || result instanceof Undefined) {
+          if (isDetailed()) {
+            logDetailed(
+                BaseMessages.getString(
+                    PKG, "ScriptValuesMod.Log.FieldNotSetByScript", 
field.getName()));
+          }
+          return null;
+        }
         return (result == null)
             ? null
             : JavaScriptUtils.convertFromJs(result, field.getType(), 
field.getName());
@@ -492,11 +490,11 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
           Script endScript = data.context.compileString(strEndScript, 
"pipeline_End", 1, null);
           endScript.exec(data.context, data.scope, data.scope);
           if (isDetailed()) {
-            logDetailed(("End Script found!"));
+            logDetailed(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.EndScriptExecuted"));
           }
         } else {
           if (isDetailed()) {
-            logDetailed(("No end Script found!"));
+            logDetailed(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.NoEndScriptDefined"));
           }
         }
       }
@@ -534,10 +532,13 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
     //
     jsScripts.clear();
     meta.getJsScripts().forEach(jsScript -> jsScripts.add(new 
ScriptValuesScript(jsScript)));
+    boolean transformScriptFound = false;
     for (ScriptValuesScript jsScript : jsScripts) {
       switch (jsScript.getType()) {
         case ScriptValuesScript.TRANSFORM_SCRIPT:
-          strTransformScript = jsScript.getScript();
+          // never null: determineUsedFields() scans this for the incoming 
field names
+          strTransformScript = Const.NVL(jsScript.getScript(), "");
+          transformScriptFound = true;
           break;
         case ScriptValuesScript.START_SCRIPT:
           strStartScript = jsScript.getScript();
@@ -549,6 +550,15 @@ public class ScriptValues extends 
BaseTransform<ScriptValuesMeta, ScriptValuesDa
           break;
       }
     }
+
+    // Without a script marked as the transform script there is nothing to run 
for the incoming
+    // rows: we would silently pass every row through and leave the output 
fields unset. That state
+    // can't be produced by the dialog, so it always means the metadata is 
broken. Fail loudly.
+    //
+    if (!transformScriptFound) {
+      logError(BaseMessages.getString(PKG, 
"ScriptValuesMod.Log.NoTransformScript"));
+      return false;
+    }
     return true;
   }
 
diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
index 9da945a3e6..431fec0128 100644
--- 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
@@ -577,11 +577,12 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
   }
 
   private void setActiveCtab(String strName) {
-    if (strName.isEmpty()) {
-      folder.setSelection(0);
-    } else {
-      folder.setSelection(getCTabPosition(strName));
-    }
+    // strName is null when none of the saved scripts is marked as the 
transform script, and
+    // getCTabPosition() returns -1 for a name that no longer matches a tab. 
Fall back to the
+    // first tab in both cases rather than blowing up while opening the dialog.
+    //
+    int position = Utils.isEmpty(strName) ? -1 : getCTabPosition(strName);
+    folder.setSelection(Math.max(position, 0));
   }
 
   private void addCtab(String cScriptName, String strScript, int iType) {
@@ -692,6 +693,31 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
     return -1;
   }
 
+  /**
+   * The script type is carried by the tab icon, which is what {@link 
#getInfo(ScriptValuesMeta)}
+   * writes out. Look it up the same way, so validation can never disagree 
with what gets saved.
+   */
+  private CTabItem getTransformScriptTab() {
+    for (CTabItem item : folder.getItems()) {
+      if (imageActiveScript.equals(item.getImage())) {
+        return item;
+      }
+    }
+    return null;
+  }
+
+  /** Make the given tab the one and only transform script. */
+  private void setTransformScriptTab(CTabItem target) {
+    for (CTabItem item : folder.getItems()) {
+      if (item == target) {
+        item.setImage(imageActiveScript);
+      } else if (imageActiveScript.equals(item.getImage())) {
+        item.setImage(imageInactiveScript);
+      }
+    }
+    strActiveScript = target.getText();
+  }
+
   private CTabItem getCTabItemByName(String strTabName) {
     CTabItem[] cItems = folder.getItems();
     for (CTabItem cItem : cItems) {
@@ -957,14 +983,13 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
     boolean bInputOK = false;
 
     // Check if Active Script has set, otherwise Ask
-    if (getCTabItemByName(strActiveScript) == null) {
+    if (getTransformScriptTab() == null) {
       MessageBox mb = new MessageBox(shell, SWT.OK | SWT.CANCEL | 
SWT.ICON_ERROR);
       mb.setMessage(BaseMessages.getString(PKG, 
"ScriptValuesDialogMod.NoActiveScriptSet"));
       mb.setText(BaseMessages.getString(PKG, 
"ScriptValuesDialogMod.ERROR.Label"));
       switch (mb.open()) {
         case SWT.OK:
-          strActiveScript = folder.getItem(0).getText();
-          refresh();
+          setTransformScriptTab(folder.getItem(0));
           bInputOK = true;
           break;
         case SWT.CANCEL:
diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
index 5618b4f50e..6d61b03a9b 100644
--- 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMeta.java
@@ -164,7 +164,12 @@ public class ScriptValuesMeta extends 
BaseTransformMeta<ScriptValues, ScriptValu
   }
 
   public ScriptValuesMeta(ScriptValuesMeta m) {
-    this();
+    // Deliberately not calling this(): the no-arg constructor seeds a default 
"Script 1" for new
+    // transforms, and copying on top of that would add one more phantom 
script on every clone.
+    //
+    super();
+    jsScripts = new ArrayList<>();
+    scriptFields = new ArrayList<>();
     this.optimizationLevel = m.optimizationLevel;
     this.languageVersion = m.languageVersion;
     m.jsScripts.forEach(s -> this.jsScripts.add(new ScriptValuesScript(s)));
@@ -291,10 +296,12 @@ public class ScriptValuesMeta extends 
BaseTransformMeta<ScriptValues, ScriptValu
     String strActiveEndScript = "";
 
     // Building the Scripts
+    boolean transformScriptFound = false;
     if (!jsScripts.isEmpty()) {
       for (ScriptValuesScript jsScript : jsScripts) {
         if (jsScript.isTransformScript()) {
           strActiveScript = jsScript.getScript();
+          transformScriptFound = true;
         } else if (jsScript.isStartScript()) {
           strActiveStartScriptName = jsScript.getName();
           strActiveStartScript = jsScript.getScript();
@@ -305,6 +312,17 @@ public class ScriptValuesMeta extends 
BaseTransformMeta<ScriptValues, ScriptValu
       }
     }
 
+    // A transform script is what gets executed for every row. Without one the 
transform quietly
+    // does nothing at all, so report it here instead of letting it slip 
through unnoticed.
+    //
+    if (!transformScriptFound) {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(PKG, 
"ScriptValuesMetaMod.CheckResult.NoTransformScript"),
+              transformMeta));
+    }
+
     if (prev != null && !strActiveScript.isEmpty()) {
       cr =
           new CheckResult(
diff --git 
a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/jsFunctionHelp.xml
 
b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/jsFunctionHelp.xml
index 64bd946f78..cc3ddd0d45 100644
--- 
a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/jsFunctionHelp.xml
+++ 
b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/jsFunctionHelp.xml
@@ -164,15 +164,32 @@
         </Arguments>
     </jsFunction>
     <jsFunction name="replace">
-        <help>Searches inside the given String(arg1) for the matching 
one(arg2) and repalces it with arg3.</help>
+        <help>Searches inside the given String(arg1) for the matching 
one(arg2) and replaces every occurrence with arg3. IMPORTANT: the value to 
search for is a regular expression, not plain text, and all matches are 
replaced.</help>
         <sample>// Replaces Strings inside the given Variable.
             // It is possible to make more replacements inside
             // one call. The given variables must be an odd number
             //
+            // IMPORTANT: the value to search for is treated as a
+            // REGULAR EXPRESSION and every match is replaced, not
+            // just the first one. Characters like . * + ? [ ] ( ) $ ^
+            // have a special meaning. A dot matches ANY character, so
+            // replace(filename, "." + extension, "") does not simply
+            // strip the extension: for "mypdf.pdf" the pattern ".pdf"
+            // already matches "ypdf" and the result becomes "m".
+            //
+            // Only pass a pattern you wrote yourself. Building one from
+            // a field value is unsafe, because that value can contain
+            // characters the regular expression engine reads as syntax.
+            // To cut a piece of literal text off a string, use plain
+            // string handling instead:
+            //
+            // var cut = filename.lastIndexOf("." + extension);
+            // var withoutExtension = cut &lt; 0 ? filename : 
filename.substring(0, cut);
+            //
             // Usage:
             // replace(var, var, var);
             // 1: String - The Variable with the content to replace.
-            // 2: String - The Value to search for.
+            // 2: String - The regular expression to search for.
             // 3: String - The Value to replace with.
             //
             // replace(var, var, var, var,var,...)
diff --git 
a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
 
b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
index 80984b56f7..046208ce9d 100644
--- 
a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
+++ 
b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
@@ -80,6 +80,7 @@ 
ScriptValuesMetaMod.CheckResult.CouldNotExecuteScript2=General error executing s
 
ScriptValuesMetaMod.CheckResult.CouldNotGetFieldsFromPreviousTransform=Couldn''t
 get fields from previous transforms, please connect all needed hops\!
 ScriptValuesMetaMod.CheckResult.FailedToGetValues=Trying to retrieve {0} 
fields\: 
 ScriptValuesMetaMod.CheckResult.NoInputReceived=No input received from other 
transforms\!
+ScriptValuesMetaMod.CheckResult.NoTransformScript=None of the scripts is 
marked as the transform script, so nothing will be executed for the incoming 
rows. Right-click the tab holding your script and choose "Set transform script".
 ScriptValuesMetaMod.CheckResult.ScriptCompiledOK=Script compiled without a 
problem
 ScriptValuesMetaMod.CheckResult.ScriptCompiledOK2=Script executed without a 
problem
 ScriptValuesMetaMod.Exception.FieldToReplaceNotFound=Field to replace [{0}] 
was not found in the transform input fields list.
@@ -117,8 +118,16 @@ ScriptValuesMod.Log.CouldNotAttachAdditionalScripts=Could 
not attach additional
 ScriptValuesMod.Log.CouldNotCompileJavascript=Couldn''t compile javascript\: 
 ScriptValuesMod.Log.ErrorProcessingStartScript=Error processing start script
 ScriptValuesMod.Log.ErrorStackTrace=Error stack trace\: 
+ScriptValuesMod.Log.EndScriptExecuted=End script executed.
+ScriptValuesMod.Log.FieldNotSetByScript=The script did not assign a value to 
output field [{0}], it is set to null.
 ScriptValuesMod.Log.JavascriptError=Javascript error\: 
 ScriptValuesMod.Log.LineNumber=linenr
+ScriptValuesMod.Log.NoEndScriptDefined=No end script defined, nothing to 
execute after the last row.
+ScriptValuesMod.Log.NoStartScriptDefined=No start script defined, nothing to 
execute before the first row.
+ScriptValuesMod.Log.NoTransformScript=None of the scripts is marked as the 
transform script, so there is nothing to execute for every row. Open the 
transform, right-click the tab holding your script and choose "Set transform 
script".
+ScriptValuesMod.Log.PipelineStatusNotUsed=The script does not set 
pipeline_Status, pipeline status checking is not used.
+ScriptValuesMod.Log.PipelineStatusUsed=The script sets pipeline_Status, the 
pipeline status is checked after every row.
+ScriptValuesMod.Log.StartScriptExecuted=Start script executed.
 ScriptValuesMod.Log.UnexpectedeError=Unexpected error
 ScriptValuesMod.Log.UsedValueName=Value \#{0} -  [{1}] is used in the script.
 ScriptValuesMod.Log.UsingValuesFromInputStream=This script is using {0} values 
from the input stream(s)
diff --git 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
index 57a8c78090..99036d9a1c 100644
--- 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
+++ 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesMetaTest.java
@@ -94,4 +94,52 @@ class ScriptValuesMetaTest {
     assertEquals(2, f.getPrecision());
     assertTrue(f.isReplace());
   }
+
+  /**
+   * The copy constructor used to call the no-arg one, which seeds a default 
"Script 1" for brand
+   * new transforms. Every clone therefore added another phantom script.
+   */
+  @Test
+  void cloningDoesNotAddPhantomScripts() {
+    ScriptValuesMeta meta = new ScriptValuesMeta();
+    meta.getJsScripts().clear();
+    meta.getJsScripts()
+        .add(new ScriptValuesScript(ScriptValuesScript.TRANSFORM_SCRIPT, 
"Script 1", "a=1;"));
+
+    ScriptValuesMeta copy = (ScriptValuesMeta) meta.clone();
+    for (int i = 0; i < 5; i++) {
+      copy = (ScriptValuesMeta) copy.clone();
+    }
+
+    assertEquals(1, copy.getJsScripts().size());
+    ScriptValuesScript script = copy.getJsScripts().getFirst();
+    assertEquals("Script 1", script.getName());
+    assertEquals("a=1;", script.getScript());
+    assertEquals(ScriptValuesScript.TRANSFORM_SCRIPT, script.getType());
+  }
+
+  /** A clone has to be a deep copy: editing the copy may not touch the 
original. */
+  @Test
+  void cloningCopiesScriptsAndFields() {
+    ScriptValuesMeta meta = new ScriptValuesMeta();
+    meta.getJsScripts().clear();
+    meta.getJsScripts()
+        .add(new ScriptValuesScript(ScriptValuesScript.TRANSFORM_SCRIPT, 
"Script 1", "a=1;"));
+    ScriptValuesMeta.ScriptField field = new ScriptValuesMeta.ScriptField();
+    field.setName("a");
+    meta.getScriptFields().add(field);
+    meta.setOptimizationLevel("3");
+    meta.setLanguageVersion("1.8");
+
+    ScriptValuesMeta copy = (ScriptValuesMeta) meta.clone();
+    assertEquals("3", copy.getOptimizationLevel());
+    assertEquals("1.8", copy.getLanguageVersion());
+    assertEquals(1, copy.getScriptFields().size());
+    assertEquals("a", copy.getScriptFields().getFirst().getName());
+
+    copy.getJsScripts().getFirst().setScript("b=2;");
+    copy.getScriptFields().getFirst().setName("b");
+    assertEquals("a=1;", meta.getJsScripts().getFirst().getScript());
+    assertEquals("a", meta.getScriptFields().getFirst().getName());
+  }
 }
diff --git 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesTest.java
 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesTest.java
index 5ef8d0cf69..263ef33e29 100644
--- 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesTest.java
+++ 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesTest.java
@@ -17,6 +17,11 @@
 
 package org.apache.hop.pipeline.transforms.javascript;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
@@ -51,6 +56,11 @@ class ScriptValuesTest {
    */
   private static ScriptValues newTransform(ScriptValuesMeta meta, RowMeta 
inputRowMeta)
       throws Exception {
+    return newTransform(meta, inputRowMeta, new ScriptValuesData());
+  }
+
+  private static ScriptValues newTransform(
+      ScriptValuesMeta meta, RowMeta inputRowMeta, ScriptValuesData data) 
throws Exception {
     TransformMockHelper<ScriptValuesMeta, ScriptValuesData> mockHelper =
         new TransformMockHelper<>("test", ScriptValuesMeta.class, 
ScriptValuesData.class);
     when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
@@ -59,16 +69,25 @@ class ScriptValuesTest {
 
     ScriptValues real =
         new ScriptValues(
-            mockHelper.transformMeta,
-            meta,
-            new ScriptValuesData(),
-            0,
-            mockHelper.pipelineMeta,
-            mockHelper.pipeline);
+            mockHelper.transformMeta, meta, data, 0, mockHelper.pipelineMeta, 
mockHelper.pipeline);
     real.setInputRowMeta(inputRowMeta);
     return spy(real);
   }
 
+  private static ScriptValuesMeta metaWith(int scriptType, String script, 
String outputField) {
+    ScriptValuesMeta meta = new ScriptValuesMeta();
+    meta.getJsScripts().clear();
+    meta.getJsScripts().add(new ScriptValuesScript(scriptType, "Script 1", 
script));
+    if (outputField != null) {
+      ScriptValuesMeta.ScriptField field = new ScriptValuesMeta.ScriptField();
+      field.setName(outputField);
+      field.setType(IValueMeta.TYPE_STRING);
+      field.setReplace(false);
+      meta.getScriptFields().add(field);
+    }
+    return meta;
+  }
+
   @Test
   void bigNumberAreNotTrimmedToInt() throws Exception {
     RowMeta input = new RowMeta();
@@ -132,4 +151,116 @@ class ScriptValuesTest {
     Object[] row = PipelineTestingUtil.execute(transform, 1, false).get(0);
     PipelineTestingUtil.assertResult(expectedRow, row);
   }
+
+  /**
+   * When no script is marked as the transform script there is nothing to run 
for the incoming rows.
+   * That used to be silent: rows passed straight through and the output 
fields were filled with
+   * Rhino's NOT_FOUND sentinel, with 0 errors reported.
+   */
+  @Test
+  void noTransformScriptFailsInit() throws Exception {
+    RowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("short_filename"));
+
+    ScriptValuesMeta meta =
+        metaWith(
+            ScriptValuesScript.NORMAL_SCRIPT,
+            "new_destination = short_filename + '.bak';",
+            "new_destination");
+
+    assertFalse(newTransform(meta, input).init());
+  }
+
+  /** A start or end script alone is not enough either, the per row script is 
what counts. */
+  @Test
+  void startScriptOnlyFailsInit() throws Exception {
+    RowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("short_filename"));
+
+    ScriptValuesMeta meta = metaWith(ScriptValuesScript.START_SCRIPT, "var 
counter = 0;", null);
+
+    assertFalse(newTransform(meta, input).init());
+  }
+
+  /** An intentionally blank transform script stays allowed, it simply passes 
rows through. */
+  @Test
+  void blankTransformScriptStillInitializes() throws Exception {
+    RowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("short_filename"));
+
+    ScriptValuesMeta meta = metaWith(ScriptValuesScript.TRANSFORM_SCRIPT, "", 
null);
+
+    assertTrue(newTransform(meta, input).init());
+  }
+
+  /**
+   * An output field the script never assigns has to come out as null. It used 
to be stringified
+   * into the row as "org.mozilla.javascript.UniqueTag@1234: NOT_FOUND".
+   */
+  @Test
+  void fieldNotSetByScriptBecomesNull() throws Exception {
+    RowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("short_filename"));
+
+    ScriptValuesMeta meta =
+        metaWith(ScriptValuesScript.TRANSFORM_SCRIPT, "var somethingElse = 
1;", "new_destination");
+
+    ScriptValues transform = newTransform(meta, input);
+    doReturn(new Object[] {"myfile"}).when(transform).getRow();
+    assertTrue(transform.init());
+
+    Object[] row = PipelineTestingUtil.execute(transform, 1, false).get(0);
+    assertEquals("myfile", row[0]);
+    assertNull(row[1]);
+    assertEquals(0, transform.getErrors());
+  }
+
+  /** Explicitly assigning undefined must not leak the JavaScript sentinel 
either. */
+  @Test
+  void undefinedFieldBecomesNull() throws Exception {
+    RowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("short_filename"));
+
+    ScriptValuesMeta meta =
+        metaWith(
+            ScriptValuesScript.TRANSFORM_SCRIPT,
+            "var new_destination = undefined;",
+            "new_destination");
+
+    ScriptValues transform = newTransform(meta, input);
+    doReturn(new Object[] {"myfile"}).when(transform).getRow();
+    assertTrue(transform.init());
+
+    Object[] row = PipelineTestingUtil.execute(transform, 1, false).get(0);
+    assertNull(row[1]);
+    assertEquals(0, transform.getErrors());
+  }
+
+  /**
+   * Fields used by the script were counted case insensitively but bound case 
sensitively, so a
+   * field only matching on case left an unusable slot pointing at field 0.
+   */
+  @Test
+  void usedFieldsAreDeterminedCaseSensitively() throws Exception {
+    RowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("Archive_Path")); // referenced 
with other casing
+    input.addValueMeta(new ValueMetaString("short_filename")); // referenced 
exactly
+
+    ScriptValuesMeta meta =
+        metaWith(
+            ScriptValuesScript.TRANSFORM_SCRIPT,
+            // the script mentions archive_path, but the field is really 
called Archive_Path
+            "// archive_path is not the real field name\nnew_destination = 
short_filename;",
+            "new_destination");
+
+    ScriptValuesData data = new ScriptValuesData();
+    ScriptValues transform = newTransform(meta, input, data);
+    doReturn(new Object[] {"C:\\archive", "myfile"}).when(transform).getRow();
+    assertTrue(transform.init());
+
+    PipelineTestingUtil.execute(transform, 1, false);
+
+    // only short_filename is really usable, index 1 in the input row
+    assertArrayEquals(new int[] {1}, data.fieldsUsed);
+  }
 }

Reply via email to