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 < 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);
+ }
}