This is an automated email from the ASF dual-hosted git repository.

bamaer 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 44de3a2fc7 fix empty field/null value regressions, fixes #8960 (#8962)
44de3a2fc7 is described below

commit 44de3a2fc756a3e764401d7c563bd163514d4de6
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Oct 16 14:27:22 2026 +0200

    fix empty field/null value regressions, fixes #8960 (#8962)
    
    * fix empty field/null value regressions, fixes #8960
    
    * remove setVariables change
---
 .../ActionSetVariablesBlankValueTest.java          | 141 +++++++++++++++++++
 .../actions/mysqlbulkfile/ActionMysqlBulkFile.java |  11 +-
 .../actions/mysqlbulkload/ActionMysqlBulkLoad.java |  23 ++--
 .../ActionMysqlBulkFileBlankFieldsTest.java        | 135 ++++++++++++++++++
 .../ActionMysqlBulkLoadBlankFieldsTest.java        | 131 ++++++++++++++++++
 .../transforms/orabulkloader/OraBulkLoader.java    |  15 +-
 .../OraBulkLoaderBlankFilesCommandLineTest.java    | 127 +++++++++++++++++
 .../transforms/avrooutput/AvroOutputField.java     |   7 +-
 .../avrooutput/AvroOutputBlankAvroNameTest.java    | 121 ++++++++++++++++
 .../pipeline/transforms/aws/sns/SnsNotifyMeta.java |   3 +-
 .../pipeline/transforms/aws/sqs/SqsReaderMeta.java |   5 +-
 .../aws/sns/SnsNotifyBlankCredChainTest.java       | 104 ++++++++++++++
 .../aws/sqs/SqsReaderBlankDefaultsTest.java        | 131 ++++++++++++++++++
 .../org/apache/hop/vfs/sftp/client/SftpClient.java |   2 +-
 .../actions/sftp/SftpBlankCompressionTest.java     |  94 +++++++++++++
 .../hop/pipeline/transforms/constant/Constant.java |   2 +-
 .../constant/ConstantBlankDateFormatTest.java      | 111 +++++++++++++++
 .../hop/pipeline/transforms/delete/DeleteMeta.java |   2 +-
 .../delete/DeleteBlankCommitSizeTest.java          |  56 ++++++++
 .../pipeline/transforms/excelinput/ExcelInput.java |   4 +-
 .../ExcelInputBlankErrorFoldersTest.java           | 145 +++++++++++++++++++
 .../transforms/insertupdate/InsertUpdateMeta.java  |   2 +-
 .../InsertUpdateBlankCommitSizeTest.java           |  56 ++++++++
 .../transforms/jdbcmetadata/JdbcMetadataMeta.java  |   4 +-
 .../jdbcmetadata/JdbcMetadataBlankRenameTest.java  |  89 ++++++++++++
 .../hop/pipeline/transforms/update/UpdateMeta.java |   2 +-
 .../update/UpdateBlankCommitSizeTest.java          |  56 ++++++++
 .../transforms/valuemapper/ValueMapperMeta.java    |   4 +-
 .../ValueMapperBlankTargetLengthTest.java          | 102 ++++++++++++++
 .../transforms/webservices/WebService.java         |  12 +-
 .../transforms/webservices/WebServiceDialog.java   |   4 +-
 .../transforms/webservices/WebServiceMeta.java     |   3 +-
 .../WebServiceBlankContainerRequestTest.java       | 142 +++++++++++++++++++
 .../hop/pipeline/transforms/xml/addxml/AddXml.java |   2 +-
 .../pipeline/transforms/xml/xmljoin/XmlJoin.java   |   3 +-
 .../xml/addxml/AddXmlBlankEncodingTest.java        | 137 ++++++++++++++++++
 .../xml/xmljoin/XmlJoinBlankEncodingTest.java      | 153 +++++++++++++++++++++
 37 files changed, 2091 insertions(+), 50 deletions(-)

diff --git 
a/plugins/actions/setvariables/src/test/java/org/apache/hop/workflow/actions/setvariables/ActionSetVariablesBlankValueTest.java
 
b/plugins/actions/setvariables/src/test/java/org/apache/hop/workflow/actions/setvariables/ActionSetVariablesBlankValueTest.java
new file mode 100644
index 0000000000..730e155084
--- /dev/null
+++ 
b/plugins/actions/setvariables/src/test/java/org/apache/hop/workflow/actions/setvariables/ActionSetVariablesBlankValueTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.workflow.actions.setvariables;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionMeta;
+import 
org.apache.hop.workflow.actions.setvariables.ActionSetVariables.VariableDefinition;
+import 
org.apache.hop.workflow.actions.setvariables.ActionSetVariables.VariableType;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
+import org.apache.hop.workflow.engines.local.LocalWorkflowEngine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/**
+ * A blank value sets the variable to an empty string, the same way whether 
the action was just
+ * edited in the dialog (which stores an empty string) or loaded from a saved 
workflow (an empty
+ * {@code <variable_value/>} tag). Up to 2.17 a saved blank value loaded as 
null and unset the
+ * variable instead, so the same workflow behaved differently after it was 
reopened.
+ */
+class ActionSetVariablesBlankValueTest {
+  private static final String WORKFLOW_VAR = 
"SET_VARIABLES_BLANK_TEST_WORKFLOW";
+  private static final String JVM_VAR = "hop.test.setvariables.blank.jvm";
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopLogStore.init();
+    HopClientEnvironment.init();
+  }
+
+  @AfterEach
+  void tearDown() {
+    System.clearProperty(JVM_VAR);
+  }
+
+  @Test
+  void blankValueFromDialogSetsAnEmptyVariable() throws Exception {
+    ActionSetVariables action = new ActionSetVariables();
+    action.setVariableDefinitions(definitions(""));
+
+    assertVariablesAre("", run(action));
+  }
+
+  @Test
+  void blankValueLoadedFromWorkflowSetsAnEmptyVariable() throws Exception {
+    assertVariablesAre("", run(loadAction(actionXml("<variable_value/>"))));
+  }
+
+  @Test
+  void blankValueSurvivesSaveAndReload() throws Exception {
+    ActionSetVariables edited = new ActionSetVariables();
+    edited.setName("set");
+    edited.setVariableDefinitions(definitions(""));
+
+    ActionSetVariables reloaded = loadAction("<action>" + edited.getXml() + 
"</action>");
+
+    assertVariablesAre("", run(reloaded));
+  }
+
+  @Test
+  void specifiedValueSetsTheVariable() throws Exception {
+    assertVariablesAre("new", 
run(loadAction(actionXml("<variable_value>new</variable_value>"))));
+  }
+
+  private static List<VariableDefinition> definitions(String value) {
+    return List.of(
+        new VariableDefinition(WORKFLOW_VAR, value, 
VariableType.CURRENT_WORKFLOW),
+        new VariableDefinition(JVM_VAR, value, VariableType.JVM));
+  }
+
+  private static String actionXml(String valueXml) {
+    return "<action><name>set</name><type>SET_VARIABLES</type>"
+        + 
"<replacevars>Y</replacevars><file_variable_type>CURRENT_WORKFLOW</file_variable_type>"
+        + "<fields>"
+        + "<field><variable_name>"
+        + WORKFLOW_VAR
+        + "</variable_name>"
+        + valueXml
+        + "<variable_type>CURRENT_WORKFLOW</variable_type></field>"
+        + "<field><variable_name>"
+        + JVM_VAR
+        + "</variable_name>"
+        + valueXml
+        + "<variable_type>JVM</variable_type></field>"
+        + "</fields></action>";
+  }
+
+  private static ActionSetVariables loadAction(String xml) throws Exception {
+    Node actionNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"action");
+    ActionSetVariables action = new ActionSetVariables();
+    action.loadXml(actionNode, new MemoryMetadataProvider(), new Variables());
+    return action;
+  }
+
+  private static IWorkflowEngine<WorkflowMeta> run(ActionSetVariables action) 
throws Exception {
+    IWorkflowEngine<WorkflowMeta> workflow = new LocalWorkflowEngine(new 
WorkflowMeta());
+    workflow.getWorkflowMeta().addAction(new ActionMeta(action));
+    action.setParentWorkflow(workflow);
+    workflow.setStopped(false);
+    workflow.setVariable(WORKFLOW_VAR, "old");
+    action.setVariable(WORKFLOW_VAR, "old");
+    System.setProperty(JVM_VAR, "old");
+
+    Result result = action.execute(new Result(), 0);
+
+    assertTrue(result.getResult());
+    return workflow;
+  }
+
+  private static void assertVariablesAre(String expected, 
IWorkflowEngine<WorkflowMeta> workflow) {
+    assertEquals(expected, workflow.getVariable(WORKFLOW_VAR), "workflow 
variable");
+    assertEquals(expected, System.getProperty(JVM_VAR), "JVM property");
+  }
+}
diff --git 
a/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFile.java
 
b/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFile.java
index 33c973d82e..09f49788a2 100644
--- 
a/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFile.java
+++ 
b/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFile.java
@@ -33,6 +33,7 @@ import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.exception.HopFileException;
 import org.apache.hop.core.util.StringUtil;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.i18n.BaseMessages;
@@ -241,7 +242,7 @@ public class ActionMysqlBulkFile extends ActionBase {
               }
 
               // Add schemaname (Most the time Schemaname.Tablename)
-              if (schemaName != null) {
+              if (!Utils.isEmpty(realSchemaname)) {
                 realTablename = realSchemaname + "." + realTablename;
               }
 
@@ -251,18 +252,18 @@ public class ActionMysqlBulkFile extends ActionBase {
               }
 
               // Set list of Column, if null get all columns (*)
-              if (getRealListColumn() != null) {
+              if (!Utils.isEmpty(getRealListColumn())) {
                 listOfColumn = mysqlString(getRealListColumn());
               }
 
               // Fields separator
-              if (getRealSeparator() != null && outDumpValue == 0) {
+              if (!Utils.isEmpty(getRealSeparator()) && outDumpValue == 0) {
                 fieldSeparator =
                     "FIELDS TERMINATED BY '" + 
Const.replace(getRealSeparator(), "'", "''") + "'";
               }
 
               // Lines Terminated by
-              if (getRealLineterminated() != null && outDumpValue == 0) {
+              if (!Utils.isEmpty(getRealLineterminated()) && outDumpValue == 
0) {
                 linesTerminated =
                     "LINES TERMINATED BY '"
                         + Const.replace(getRealLineterminated(), "'", "''")
@@ -274,7 +275,7 @@ public class ActionMysqlBulkFile extends ActionBase {
                 strHighPriority = "HIGH_PRIORITY";
               }
 
-              if (getRealEnclosed() != null && outDumpValue == 0) {
+              if (!Utils.isEmpty(getRealEnclosed()) && outDumpValue == 0) {
                 if (isOptionEnclosed()) {
                   optionEnclosed = "OPTIONALLY ";
                 }
diff --git 
a/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
 
b/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
index 9a7f62668e..d66ee57197 100644
--- 
a/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
+++ 
b/plugins/databases/mysql/src/main/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoad.java
@@ -214,7 +214,7 @@ public class ActionMysqlBulkLoad extends ActionBase {
                 }
 
                 // Add schemaname (Most the time Schemaname.Tablename)
-                if (schemaName != null) {
+                if (!Utils.isEmpty(realSchemaname)) {
                   realTablename = realSchemaname + "." + realTablename;
                 }
 
@@ -231,7 +231,7 @@ public class ActionMysqlBulkLoad extends ActionBase {
                 }
 
                 // Set list of Column
-                if (getRealListattribut() != null) {
+                if (!Utils.isEmpty(getRealListattribut())) {
                   listOfColumn = "(" + mysqlString(getRealListattribut()) + 
")";
                 }
 
@@ -250,26 +250,26 @@ public class ActionMysqlBulkLoad extends ActionBase {
                 }
 
                 // Fields ....
-                if (getRealSeparator() != null
-                    || getRealEnclosed() != null
-                    || getRealEscaped() != null) {
+                if (!Utils.isEmpty(getRealSeparator())
+                    || !Utils.isEmpty(getRealEnclosed())
+                    || !Utils.isEmpty(getRealEscaped())) {
                   fieldTerminatedby = "FIELDS ";
 
-                  if (getRealSeparator() != null) {
+                  if (!Utils.isEmpty(getRealSeparator())) {
                     fieldTerminatedby =
                         fieldTerminatedby
                             + "TERMINATED BY '"
                             + Const.replace(getRealSeparator(), "'", "''")
                             + "'";
                   }
-                  if (getRealEnclosed() != null) {
+                  if (!Utils.isEmpty(getRealEnclosed())) {
                     fieldTerminatedby =
                         fieldTerminatedby
                             + " ENCLOSED BY '"
                             + Const.replace(getRealEnclosed(), "'", "''")
                             + "'";
                   }
-                  if (getRealEscaped() != null) {
+                  if (!Utils.isEmpty(getRealEscaped())) {
 
                     fieldTerminatedby =
                         fieldTerminatedby
@@ -280,11 +280,12 @@ public class ActionMysqlBulkLoad extends ActionBase {
                 }
 
                 // LINES ...
-                if (getRealLinestarted() != null || getRealLineterminated() != 
null) {
+                if (!Utils.isEmpty(getRealLinestarted())
+                    || !Utils.isEmpty(getRealLineterminated())) {
                   lineTerminatedby = "LINES ";
 
                   // Line starting By
-                  if (getRealLinestarted() != null) {
+                  if (!Utils.isEmpty(getRealLinestarted())) {
                     lineTerminatedby =
                         lineTerminatedby
                             + "STARTING BY '"
@@ -293,7 +294,7 @@ public class ActionMysqlBulkLoad extends ActionBase {
                   }
 
                   // Line terminating By
-                  if (getRealLineterminated() != null) {
+                  if (!Utils.isEmpty(getRealLineterminated())) {
                     lineTerminatedby =
                         lineTerminatedby
                             + " TERMINATED BY '"
diff --git 
a/plugins/databases/mysql/src/test/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFileBlankFieldsTest.java
 
b/plugins/databases/mysql/src/test/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFileBlankFieldsTest.java
new file mode 100644
index 0000000000..e97aa20d4f
--- /dev/null
+++ 
b/plugins/databases/mysql/src/test/java/org/apache/hop/workflow/actions/mysqlbulkfile/ActionMysqlBulkFileBlankFieldsTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.workflow.actions.mysqlbulkfile;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.file.Path;
+import java.sql.PreparedStatement;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
+import org.w3c.dom.Node;
+
+/**
+ * Blank schema, column list, separator, enclosure and line settings must be 
left out of the
+ * generated SELECT ... INTO OUTFILE statement. Up to 2.17 a saved empty tag 
such as {@code
+ * <schemaname/>} loaded as null; since 2.18 it loads as an empty string, 
which must not produce
+ * {@code FROM .mytable}, an empty column list or {@code TERMINATED BY ''}.
+ */
+class ActionMysqlBulkFileBlankFieldsTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @TempDir Path tempDir;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankSettingsAreLeftOutOfTheStatement() throws Exception {
+    String sql =
+        
runAndCaptureSql("<schemaname/><listcolumn/><separator/><enclosed/><lineterminated/>");
+
+    assertTrue(sql.startsWith("SELECT  * INTO OUTFILE '"), sql);
+    assertTrue(sql.contains(" FROM mytable "), sql);
+    assertFalse(sql.contains(".mytable"), sql);
+    assertFalse(sql.contains("FIELDS"), sql);
+    assertFalse(sql.contains("ENCLOSED"), sql);
+    assertFalse(sql.contains("LINES"), sql);
+    assertFalse(sql.contains("BY ''"), sql);
+    assertFalse(sql.contains("``"), sql);
+  }
+
+  @Test
+  void specifiedSettingsAreInTheStatement() throws Exception {
+    String sql =
+        runAndCaptureSql(
+            "<schemaname>myschema</schemaname><listcolumn>a,b</listcolumn>"
+                + "<separator>;</separator><enclosed>\"</enclosed>"
+                + "<lineterminated>|</lineterminated>");
+
+    assertTrue(sql.startsWith("SELECT  `a`, `b` INTO OUTFILE '"), sql);
+    assertTrue(
+        sql.contains("FIELDS TERMINATED BY ';' ENCLOSED BY '\"' LINES 
TERMINATED BY '|'"), sql);
+    assertTrue(sql.contains(" FROM myschema.mytable "), sql);
+  }
+
+  private String runAndCaptureSql(String settingsXml) throws Exception {
+    Path outFile = tempDir.resolve("out.txt");
+    String xml =
+        "<action><name>bulkfile</name><type>MYSQL_BULK_FILE</type>"
+            + "<connection>mysql</connection>"
+            + "<tablename>mytable</tablename>"
+            + "<filename>"
+            + outFile.toAbsolutePath()
+            + "</filename>"
+            + "<limitlines>0</limitlines><highpriority>N</highpriority>"
+            + 
"<optionenclosed>N</optionenclosed><outdumpvalue>0</outdumpvalue>"
+            + 
"<iffileexists>2</iffileexists><addfiletoresult>N</addfiletoresult>"
+            + settingsXml
+            + "</action>";
+    Node actionNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"action");
+    ActionMysqlBulkFile action = new ActionMysqlBulkFile();
+    action.loadXml(actionNode, new MemoryMetadataProvider(), new Variables());
+
+    WorkflowMeta workflowMeta = mock(WorkflowMeta.class);
+    when(workflowMeta.findDatabase(eq("mysql"), 
any())).thenReturn(mock(DatabaseMeta.class));
+    action.setParentWorkflowMeta(workflowMeta);
+
+    try (MockedConstruction<Database> databases =
+        mockConstruction(
+            Database.class,
+            (db, context) -> {
+              when(db.checkTableExists(any(), any())).thenReturn(true);
+              
when(db.prepareSql(anyString())).thenReturn(mock(PreparedStatement.class));
+            })) {
+      Result result = action.execute(new Result(), 0);
+
+      assertEquals(1, databases.constructed().size());
+      ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
+      verify(databases.constructed().get(0)).prepareSql(sql.capture());
+      assertTrue(result.getResult());
+      return sql.getValue();
+    }
+  }
+}
diff --git 
a/plugins/databases/mysql/src/test/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoadBlankFieldsTest.java
 
b/plugins/databases/mysql/src/test/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoadBlankFieldsTest.java
new file mode 100644
index 0000000000..548420f7a1
--- /dev/null
+++ 
b/plugins/databases/mysql/src/test/java/org/apache/hop/workflow/actions/mysqlbulkload/ActionMysqlBulkLoadBlankFieldsTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.workflow.actions.mysqlbulkload;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
+import org.w3c.dom.Node;
+
+/**
+ * Blank schema, column list, separator, enclosure, escape and line settings 
must be left out of the
+ * generated LOAD DATA statement. Up to 2.17 a saved empty tag such as {@code 
<schemaname/>} loaded
+ * as null; since 2.18 it loads as an empty string, which must not produce 
{@code .mytable} or
+ * {@code TERMINATED BY ''}.
+ */
+class ActionMysqlBulkLoadBlankFieldsTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @TempDir Path tempDir;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankSettingsAreLeftOutOfTheStatement() throws Exception {
+    String sql =
+        runAndCaptureSql(
+            "<schemaname/><separator/><enclosed/><escaped/>"
+                + "<linestarted/><lineterminated/><listattribut/>");
+
+    assertTrue(sql.contains(" INTO TABLE mytable "), sql);
+    assertFalse(sql.contains(".mytable"), sql);
+    assertFalse(sql.contains("FIELDS"), sql);
+    assertFalse(sql.contains("BY ''"), sql);
+    assertFalse(sql.contains("LINES"), sql);
+    assertFalse(sql.contains("(``)"), sql);
+  }
+
+  @Test
+  void specifiedSettingsAreInTheStatement() throws Exception {
+    String sql =
+        runAndCaptureSql(
+            
"<schemaname>myschema</schemaname><separator>;</separator><enclosed>\"</enclosed>"
+                + "<escaped>\\</escaped><linestarted>x</linestarted>"
+                + 
"<lineterminated>|</lineterminated><listattribut>a,b</listattribut>");
+
+    assertTrue(sql.contains(" INTO TABLE myschema.mytable "), sql);
+    assertTrue(sql.contains("FIELDS TERMINATED BY ';' ENCLOSED BY '\"' ESCAPED 
BY '\\'"), sql);
+    assertTrue(sql.contains("LINES STARTING BY 'x' TERMINATED BY '|'"), sql);
+    assertTrue(sql.contains("(`a`, `b`)"), sql);
+  }
+
+  private String runAndCaptureSql(String settingsXml) throws Exception {
+    Path dataFile = Files.writeString(tempDir.resolve("data.csv"), "1;2\n");
+    String xml =
+        "<action><name>bulk</name><type>MYSQL_BULK_LOAD</type>"
+            + "<connection>mysql</connection>"
+            + "<tablename>mytable</tablename>"
+            + "<filename>"
+            + dataFile.toAbsolutePath()
+            + "</filename>"
+            + "<ignorelines>0</ignorelines><replacedata>N</replacedata>"
+            + "<localinfile>Y</localinfile><prorityvalue>0</prorityvalue>"
+            + "<addfiletoresult>N</addfiletoresult>"
+            + settingsXml
+            + "</action>";
+    Node actionNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"action");
+    ActionMysqlBulkLoad action = new ActionMysqlBulkLoad();
+    action.loadXml(actionNode, new MemoryMetadataProvider(), new Variables());
+
+    WorkflowMeta workflowMeta = mock(WorkflowMeta.class);
+    when(workflowMeta.findDatabase(eq("mysql"), 
any())).thenReturn(mock(DatabaseMeta.class));
+    action.setParentWorkflowMeta(workflowMeta);
+
+    try (MockedConstruction<Database> databases =
+        mockConstruction(
+            Database.class,
+            (db, context) -> when(db.checkTableExists(any(), 
any())).thenReturn(true))) {
+      Result result = action.execute(new Result(), 0);
+
+      assertEquals(1, databases.constructed().size());
+      ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
+      verify(databases.constructed().get(0)).execStatement(sql.capture());
+      assertTrue(result.getResult());
+      return sql.getValue();
+    }
+  }
+}
diff --git 
a/plugins/databases/oracle/src/main/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoader.java
 
b/plugins/databases/oracle/src/main/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoader.java
index 0b5b88f1b6..5a86f192bb 100644
--- 
a/plugins/databases/oracle/src/main/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoader.java
+++ 
b/plugins/databases/oracle/src/main/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoader.java
@@ -359,7 +359,7 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
   public String createCommandLine(OraBulkLoaderMeta meta, boolean password) 
throws HopException {
     StringBuilder sb = new StringBuilder(300);
 
-    if (meta.getSqlldr() != null) {
+    if (!Utils.isEmpty(meta.getSqlldr())) {
       try {
         FileObject fileObject = getFileObject(meta.getSqlldr(), variables);
         String sqlldr = getFilename(fileObject);
@@ -371,7 +371,7 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
       throw new HopException("No sqlldr application specified");
     }
 
-    if (meta.getControlFile() != null) {
+    if (!Utils.isEmpty(meta.getControlFile())) {
       try {
         FileObject fileObject = getFileObject(meta.getControlFile(), 
variables);
 
@@ -389,7 +389,7 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
       sb.append(" data=\'-\'");
     }
 
-    if (meta.getLogFile() != null) {
+    if (!Utils.isEmpty(meta.getLogFile())) {
       try {
         FileObject fileObject = getFileObject(meta.getLogFile(), variables);
 
@@ -401,7 +401,7 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
       }
     }
 
-    if (meta.getBadFile() != null) {
+    if (!Utils.isEmpty(meta.getBadFile())) {
       try {
         FileObject fileObject = getFileObject(meta.getBadFile(), variables);
 
@@ -413,7 +413,7 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
       }
     }
 
-    if (meta.getDiscardFile() != null) {
+    if (!Utils.isEmpty(meta.getDiscardFile())) {
       try {
         FileObject fileObject = getFileObject(meta.getDiscardFile(), 
variables);
 
@@ -668,7 +668,8 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
       FileObject fileObject = null;
 
       String method = meta.getLoadMethod();
-      if (OraBulkLoaderMeta.METHOD_AUTO_END.equals(method) && 
meta.getControlFile() != null) {
+      if (OraBulkLoaderMeta.METHOD_AUTO_END.equals(method)
+          && !Utils.isEmpty(meta.getControlFile())) {
 
         try {
           fileObject = getFileObject(meta.getControlFile(), variables);
@@ -681,7 +682,7 @@ public class OraBulkLoader extends 
BaseTransform<OraBulkLoaderMeta, OraBulkLoade
         }
       }
 
-      if (OraBulkLoaderMeta.METHOD_AUTO_END.equals(method) && 
meta.getDataFile() != null) {
+      if (OraBulkLoaderMeta.METHOD_AUTO_END.equals(method) && 
!Utils.isEmpty(meta.getDataFile())) {
         // In concurrent mode the data is written to the control file.
 
         try {
diff --git 
a/plugins/databases/oracle/src/test/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoaderBlankFilesCommandLineTest.java
 
b/plugins/databases/oracle/src/test/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoaderBlankFilesCommandLineTest.java
new file mode 100644
index 0000000000..2742961a7a
--- /dev/null
+++ 
b/plugins/databases/oracle/src/test/java/org/apache/hop/pipeline/transforms/orabulkloader/OraBulkLoaderBlankFilesCommandLineTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.orabulkloader;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+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.w3c.dom.Node;
+
+/**
+ * Blank log, bad and discard files must be left off the sqlldr command line. 
Up to 2.17 a saved
+ * {@code <log_file/>} loaded as null; since 2.18 it loads as an empty string, 
which must not turn
+ * into {@code log='<working directory>'}.
+ */
+class OraBulkLoaderBlankFilesCommandLineTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<OraBulkLoaderMeta, OraBulkLoaderData> helper;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() throws Exception {
+    helper =
+        new TransformMockHelper<>(
+            "Oracle bulk loader", OraBulkLoaderMeta.class, 
OraBulkLoaderData.class);
+    when(helper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(helper.iLogChannel);
+    DatabaseMeta databaseMeta = mock(DatabaseMeta.class);
+    when(databaseMeta.getUsername()).thenReturn("scott");
+    when(databaseMeta.getPassword()).thenReturn("tiger");
+    when(databaseMeta.getURL(any(IVariables.class)))
+        .thenReturn("jdbc:oracle:thin:@localhost:1521:XE");
+    when(helper.pipelineMeta.findDatabase(eq("ora"), any(IVariables.class)))
+        .thenReturn(databaseMeta);
+  }
+
+  @AfterEach
+  void tearDown() {
+    helper.cleanUp();
+  }
+
+  @Test
+  void blankLogBadAndDiscardFilesAreOmitted() throws Exception {
+    String commandLine = commandLine("<log_file/><bad_file/><discard_file/>");
+
+    assertTrue(commandLine.contains(" control='"), commandLine);
+    assertFalse(commandLine.contains(" log="), commandLine);
+    assertFalse(commandLine.contains(" bad="), commandLine);
+    assertFalse(commandLine.contains(" discard="), commandLine);
+  }
+
+  @Test
+  void specifiedLogBadAndDiscardFilesArePassed() throws Exception {
+    String commandLine =
+        commandLine(
+            "<log_file>/tmp/ora/load.log</log_file>"
+                + "<bad_file>/tmp/ora/load.bad</bad_file>"
+                + "<discard_file>/tmp/ora/load.dsc</discard_file>");
+
+    assertTrue(commandLine.matches(".* log='[^']*load\\.log'.*"), commandLine);
+    assertTrue(commandLine.matches(".* bad='[^']*load\\.bad'.*"), commandLine);
+    assertTrue(commandLine.matches(".* discard='[^']*load\\.dsc'.*"), 
commandLine);
+  }
+
+  private String commandLine(String fileTags) throws Exception {
+    String xml =
+        "<transform>"
+            + "<connection>ora</connection>"
+            + "<sqlldr>/opt/oracle/bin/sqlldr</sqlldr>"
+            + "<control_file>/tmp/ora/load.ctl</control_file>"
+            + "<data_file>/tmp/ora/load.dat</data_file>"
+            + "<load_method>AUTO_END</load_method>"
+            + fileTags
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    OraBulkLoaderMeta meta = new OraBulkLoaderMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+
+    OraBulkLoader loader =
+        new OraBulkLoader(
+            helper.transformMeta,
+            meta,
+            new OraBulkLoaderData(),
+            0,
+            helper.pipelineMeta,
+            helper.pipeline);
+    return loader.createCommandLine(meta, false);
+  }
+}
diff --git 
a/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputField.java
 
b/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputField.java
index c3003391ee..293cc85fbe 100644
--- 
a/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputField.java
+++ 
b/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputField.java
@@ -24,6 +24,7 @@ import java.util.List;
 import org.apache.avro.Schema;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 
 /** Describes a single field in a text file */
@@ -105,13 +106,13 @@ public class AvroOutputField implements Cloneable, 
Comparable<AvroOutputField> {
   }
 
   /**
-   * Get the name of the field in the Avro Schema If the Avro field name is 
null returns the stream
-   * field name
+   * Get the name of the field in the Avro Schema If the Avro field name is 
null or empty returns
+   * the stream field name
    *
    * @return avroName
    */
   public String getAvroName() {
-    return avroName != null ? avroName : name;
+    return Utils.isEmpty(avroName) ? name : avroName;
   }
 
   /**
diff --git 
a/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputBlankAvroNameTest.java
 
b/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputBlankAvroNameTest.java
new file mode 100644
index 0000000000..2c14ad03e8
--- /dev/null
+++ 
b/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrooutput/AvroOutputBlankAvroNameTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.avro.transforms.avrooutput;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import org.apache.avro.Schema;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+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.w3c.dom.Node;
+
+/**
+ * An output field with a blank Avro path must be written under its stream 
field name. Up to 2.17 a
+ * saved {@code <avroname/>} loaded as null; since 2.18 it loads as an empty 
string, which must not
+ * be used as the (empty) Avro field name.
+ */
+class AvroOutputBlankAvroNameTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<AvroOutputMeta, AvroOutputData> mockHelper;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    mockHelper =
+        new TransformMockHelper<>("AvroOutput", AvroOutputMeta.class, 
AvroOutputData.class);
+    when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(mockHelper.iLogChannel);
+  }
+
+  @AfterEach
+  void tearDown() {
+    mockHelper.cleanUp();
+  }
+
+  @Test
+  void blankAvroNameFallsBackToStreamFieldName() throws Exception {
+    AvroOutputMeta meta = loadMeta("<avroname/>");
+
+    assertEquals("id", meta.getOutputFields().get(0).getAvroName());
+
+    Schema schema = createSchema(meta);
+    assertEquals(1, schema.getFields().size());
+    assertNotNull(schema.getField("id"), "Generated schema: " + schema);
+  }
+
+  @Test
+  void specifiedAvroNameIsUsed() throws Exception {
+    AvroOutputMeta meta = loadMeta("<avroname>customer_id</avroname>");
+
+    assertEquals("customer_id", meta.getOutputFields().get(0).getAvroName());
+
+    Schema schema = createSchema(meta);
+    assertEquals(1, schema.getFields().size());
+    assertNotNull(schema.getField("customer_id"), "Generated schema: " + 
schema);
+  }
+
+  private Schema createSchema(AvroOutputMeta meta) throws HopException {
+    AvroOutput transform =
+        new AvroOutput(
+            mockHelper.transformMeta,
+            meta,
+            new AvroOutputData(),
+            0,
+            mockHelper.pipelineMeta,
+            mockHelper.pipeline);
+    return transform.createAvroSchema(new ArrayList<>(meta.getOutputFields()), 
"");
+  }
+
+  private static AvroOutputMeta loadMeta(String avroNameTag) throws Exception {
+    String xml =
+        "<transform>"
+            + "<recordname>customer</recordname>"
+            + "<namespace>org.apache.hop.test</namespace>"
+            + "<doc>test</doc>"
+            + "<fields><field>"
+            + "<name>id</name>"
+            + avroNameTag
+            + "<avrotype>5</avrotype>"
+            + "<nullable>N</nullable>"
+            + "</field></fields></transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    AvroOutputMeta meta = new AvroOutputMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyMeta.java
 
b/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyMeta.java
index 75bcb9f81e..9081dab9a4 100644
--- 
a/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyMeta.java
+++ 
b/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyMeta.java
@@ -26,6 +26,7 @@ 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.ValueMetaFactory;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
@@ -105,7 +106,7 @@ public class SnsNotifyMeta extends 
BaseTransformMeta<SnsNotify, SnsNotifyData> {
   }
 
   public String getAwsCredChain() {
-    return awsCredChain == null ? "N" : awsCredChain;
+    return Utils.isEmpty(awsCredChain) ? "N" : awsCredChain;
   }
 
   public void setAwsCredChain(String awsCredChain) {
diff --git 
a/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderMeta.java
 
b/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderMeta.java
index 33a091965b..9f8dda968e 100644
--- 
a/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderMeta.java
+++ 
b/plugins/tech/aws/src/main/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderMeta.java
@@ -23,6 +23,7 @@ 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.ValueMetaFactory;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
@@ -102,7 +103,7 @@ public class SqsReaderMeta extends 
BaseTransformMeta<SqsReader, SqsReaderData> {
   public void setDefault() {}
 
   public String getAwsCredChain() {
-    return awsCredChain = awsCredChain == null ? "N" : awsCredChain;
+    return awsCredChain = Utils.isEmpty(awsCredChain) ? "N" : awsCredChain;
   }
 
   public void setAwsCredChain(String awsCredChain) {
@@ -190,7 +191,7 @@ public class SqsReaderMeta extends 
BaseTransformMeta<SqsReader, SqsReaderData> {
   }
 
   public String getTFldMaxMessages() {
-    return tFldMaxMessages == null ? "0" : tFldMaxMessages;
+    return Utils.isEmpty(tFldMaxMessages) ? "0" : tFldMaxMessages;
   }
 
   public void setTFldMaxMessages(String tFldMaxMessages) {
diff --git 
a/plugins/tech/aws/src/test/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyBlankCredChainTest.java
 
b/plugins/tech/aws/src/test/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyBlankCredChainTest.java
new file mode 100644
index 0000000000..68e2716e36
--- /dev/null
+++ 
b/plugins/tech/aws/src/test/java/org/apache/hop/pipeline/transforms/aws/sns/SnsNotifyBlankCredChainTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.aws.sns;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.w3c.dom.Node;
+
+/**
+ * A blank "use AWS credentials chain" setting must still mean "N" (use the 
configured keys). Up to
+ * 2.17 a saved {@code <AwsCredChain/>} loaded as null; since 2.18 it loads as 
an empty string. No
+ * AWS calls are made: building an SDK client is offline.
+ */
+class SnsNotifyBlankCredChainTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankCredChainUsesConfiguredKeys() throws Exception {
+    SnsNotifyMeta meta = loadMeta("<AwsCredChain/>");
+    assertEquals("N", meta.getAwsCredChain());
+
+    BaseTransform transform = resolvingTransform();
+    assertTrue(new AwsSns(meta, new PipelineMeta(), 
transform).getAWSConnection());
+    verify(transform, never()).logBasic(contains("Credentials Chain"));
+    verify(transform).logBasic(contains("with API-Key"));
+  }
+
+  @Test
+  void specifiedCredChainIsUsed() throws Exception {
+    assertEquals("Y", 
loadMeta("<AwsCredChain>Y</AwsCredChain>").getAwsCredChain());
+
+    SnsNotifyMeta meta = loadMeta("<AwsCredChain>N</AwsCredChain>");
+    BaseTransform transform = resolvingTransform();
+    assertTrue(new AwsSns(meta, new PipelineMeta(), 
transform).getAWSConnection());
+    verify(transform, never()).logBasic(contains("Credentials Chain"));
+    verify(transform).logBasic(contains("with API-Key"));
+  }
+
+  private static BaseTransform resolvingTransform() {
+    BaseTransform transform = mock(BaseTransform.class);
+    when(transform.resolve(anyString())).thenAnswer(invocation -> 
invocation.getArgument(0));
+    return transform;
+  }
+
+  private static SnsNotifyMeta loadMeta(String credChainTag) throws Exception {
+    String xml =
+        "<transform>"
+            + credChainTag
+            + "<aws_key>key</aws_key>"
+            + "<aws_key_secret>secret</aws_key_secret>"
+            + "<aws_region>eu-west-1</aws_region>"
+            + "<notify_point>Send once with first row</notify_point>"
+            + "<chooser_topic_arn>N</chooser_topic_arn>"
+            + "<chooser_subject>N</chooser_subject>"
+            + "<chooser_message>N</chooser_message>"
+            + 
"<value_topic_arn>arn:aws:sns:eu-west-1:123456789012:topic</value_topic_arn>"
+            + "<value_subject>subject</value_subject>"
+            + "<value_message>message</value_message>"
+            + "<field_message_id>id</field_message_id>"
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    SnsNotifyMeta meta = new SnsNotifyMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/tech/aws/src/test/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderBlankDefaultsTest.java
 
b/plugins/tech/aws/src/test/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderBlankDefaultsTest.java
new file mode 100644
index 0000000000..d3e5b785bd
--- /dev/null
+++ 
b/plugins/tech/aws/src/test/java/org/apache/hop/pipeline/transforms/aws/sqs/SqsReaderBlankDefaultsTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.aws.sqs;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.w3c.dom.Node;
+
+/**
+ * Blank SQS Reader settings must fall back to their defaults. Up to 2.17 a 
saved empty tag loaded
+ * as null; since 2.18 it loads as an empty string. A blank "max messages" 
must still mean 0
+ * (unlimited) and a blank "use AWS credentials chain" must still mean "N" 
(use the configured
+ * keys). No AWS calls are made: building an SDK client is offline.
+ */
+class SqsReaderBlankDefaultsTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankSettingsFallBackToDefaults() throws Exception {
+    SqsReaderMeta meta = loadMeta("<AwsCredChain/>", "<field_max_messages/>");
+
+    assertEquals("N", meta.getAwsCredChain());
+    assertEquals("0", meta.getTFldMaxMessages());
+  }
+
+  @Test
+  void blankMaxMessagesInitializesAsUnlimited() throws Exception {
+    SqsReaderMeta meta = loadMeta("<AwsCredChain>N</AwsCredChain>", 
"<field_max_messages/>");
+    SqsReaderData data = new SqsReaderData();
+
+    assertTrue(newSqsReader(meta, data).init(), "init() must accept a blank 
max messages");
+    assertEquals(0, data.realMaxMessages);
+  }
+
+  @Test
+  void blankCredChainUsesConfiguredKeys() throws Exception {
+    SqsReaderMeta meta = loadMeta("<AwsCredChain/>", 
"<field_max_messages>0</field_max_messages>");
+    BaseTransform transform = resolvingTransform();
+
+    assertTrue(new AwsSqsReader(meta, new PipelineMeta(), 
transform).getAWSConnection());
+    verify(transform, never()).logBasic(contains("Credentials Chain"));
+  }
+
+  @Test
+  void specifiedValuesAreUsed() throws Exception {
+    SqsReaderMeta meta =
+        loadMeta("<AwsCredChain>Y</AwsCredChain>", 
"<field_max_messages>5</field_max_messages>");
+    assertEquals("Y", meta.getAwsCredChain());
+    assertEquals("5", meta.getTFldMaxMessages());
+
+    meta.setAwsCredChain("N");
+    SqsReaderData data = new SqsReaderData();
+    assertTrue(newSqsReader(meta, data).init());
+    assertEquals(5, data.realMaxMessages);
+  }
+
+  private static BaseTransform resolvingTransform() {
+    BaseTransform transform = mock(BaseTransform.class);
+    when(transform.resolve(anyString())).thenAnswer(invocation -> 
invocation.getArgument(0));
+    return transform;
+  }
+
+  private static SqsReader newSqsReader(SqsReaderMeta meta, SqsReaderData 
data) {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    TransformMeta transformMeta = new TransformMeta("sqs", meta);
+    pipelineMeta.addTransform(transformMeta);
+    Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+    return new SqsReader(transformMeta, meta, data, 0, pipelineMeta, pipeline);
+  }
+
+  private static SqsReaderMeta loadMeta(String credChainTag, String 
maxMessagesTag)
+      throws Exception {
+    String xml =
+        "<transform>"
+            + credChainTag
+            + "<aws_key>key</aws_key>"
+            + "<aws_key_secret>secret</aws_key_secret>"
+            + "<aws_region>eu-west-1</aws_region>"
+            + 
"<sqs_queue>https://sqs.eu-west-1.amazonaws.com/123456789012/queue</sqs_queue>"
+            + "<field_message_id>id</field_message_id>"
+            + "<field_message_body>body</field_message_body>"
+            + "<field_message_delete>N</field_message_delete>"
+            + maxMessagesTag
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    SqsReaderMeta meta = new SqsReaderMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/client/SftpClient.java
 
b/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/client/SftpClient.java
index 43d89df0ce..770ba97e99 100644
--- 
a/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/client/SftpClient.java
+++ 
b/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/client/SftpClient.java
@@ -403,7 +403,7 @@ public class SftpClient {
   }
 
   public String getCompression() {
-    if (this.compression == null) {
+    if (Utils.isEmpty(this.compression)) {
       return null;
     }
     if (this.compression.equals("zlib")) {
diff --git 
a/plugins/tech/sftp/src/test/java/org/apache/hop/workflow/actions/sftp/SftpBlankCompressionTest.java
 
b/plugins/tech/sftp/src/test/java/org/apache/hop/workflow/actions/sftp/SftpBlankCompressionTest.java
new file mode 100644
index 0000000000..af53d70e8d
--- /dev/null
+++ 
b/plugins/tech/sftp/src/test/java/org/apache/hop/workflow/actions/sftp/SftpBlankCompressionTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.workflow.actions.sftp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.net.InetAddress;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.vfs.sftp.client.SftpClient;
+import org.apache.hop.workflow.actions.sftpput.ActionSftpPut;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.w3c.dom.Node;
+
+/**
+ * A blank Compression on the SFTP get / put actions means no compression 
settings for JSch. Up to
+ * 2.17 a saved {@code <compression/>} loaded as null; since 2.18 it loads as 
an empty string, which
+ * must not be handed to JSch as an (empty) compression algorithm list.
+ */
+class SftpBlankCompressionTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankCompressionOnSftpGetSetsNoCompression() throws Exception {
+    ActionSftp action = new ActionSftp();
+    action.loadXml(actionNode("<compression/>"), new MemoryMetadataProvider(), 
new Variables());
+
+    assertNull(clientCompression(action.getCompression()));
+  }
+
+  @Test
+  void blankCompressionOnSftpPutSetsNoCompression() throws Exception {
+    ActionSftpPut action = new ActionSftpPut();
+    action.loadXml(actionNode("<compression/>"), new MemoryMetadataProvider(), 
new Variables());
+
+    assertNull(clientCompression(action.getCompression()));
+  }
+
+  @Test
+  void zlibCompressionIsPassedOn() throws Exception {
+    ActionSftpPut action = new ActionSftpPut();
+    action.loadXml(
+        actionNode("<compression>zlib</compression>"),
+        new MemoryMetadataProvider(),
+        new Variables());
+
+    assertEquals("[email protected],zlib", 
clientCompression(action.getCompression()));
+  }
+
+  /** The value SftpClient.login() puts into the JSch compression.s2c / 
compression.c2s config. */
+  private static String clientCompression(String actionCompression) throws 
Exception {
+    SftpClient client = new SftpClient(InetAddress.getLoopbackAddress(), 22, 
"user");
+    client.setCompression(actionCompression);
+    return client.getCompression();
+  }
+
+  private static Node actionNode(String compressionTag) throws Exception {
+    String xml =
+        "<action><name>sftp</name>"
+            + "<servername>localhost</servername><serverport>22</serverport>"
+            + "<username>user</username>"
+            + compressionTag
+            + "</action>";
+    return XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), "action");
+  }
+}
diff --git 
a/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
 
b/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
index 56cfc743d4..983f9f89b2 100644
--- 
a/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
+++ 
b/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
@@ -147,7 +147,7 @@ public class Constant extends BaseTransform<ConstantMeta, 
ConstantData> {
 
               case IValueMeta.TYPE_DATE:
                 try {
-                  if (field.getFieldFormat() != null) {
+                  if (!Utils.isEmpty(field.getFieldFormat())) {
                     data.daf.applyPattern(field.getFieldFormat());
                     data.daf.setDateFormatSymbols(data.dafs);
                   }
diff --git 
a/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantBlankDateFormatTest.java
 
b/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantBlankDateFormatTest.java
new file mode 100644
index 0000000000..fba9ab38c5
--- /dev/null
+++ 
b/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantBlankDateFormatTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.constant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.RowMetaAndData;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.w3c.dom.Node;
+
+/**
+ * A Date constant with a blank Format must be parsed with the default date 
format. Up to 2.17 a
+ * saved {@code <format/>} loaded as null; since 2.18 it loads as an empty 
string, which must not be
+ * applied as an (empty) date pattern.
+ */
+class ConstantBlankDateFormatTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankFormatParsesWithDefaultDateFormat() throws Exception {
+    // The default SimpleDateFormat pattern is locale dependent, so derive the 
constant from it.
+    String value = new SimpleDateFormat().format(may17());
+    ConstantMeta meta = loadMeta(fieldXml(value, "<format/>"));
+
+    List<ICheckResult> remarks = new ArrayList<>();
+    RowMetaAndData row = Constant.buildRow(meta, new ConstantData(), remarks);
+
+    assertTrue(remarks.isEmpty(), "Unexpected remarks: " + texts(remarks));
+    assertEquals(may17(), row.getData()[0]);
+  }
+
+  @Test
+  void specifiedFormatIsUsedToParseDate() throws Exception {
+    ConstantMeta meta = loadMeta(fieldXml("2024/05/17 10:30", 
"<format>yyyy/MM/dd HH:mm</format>"));
+
+    List<ICheckResult> remarks = new ArrayList<>();
+    RowMetaAndData row = Constant.buildRow(meta, new ConstantData(), remarks);
+
+    assertTrue(remarks.isEmpty(), "Unexpected remarks: " + texts(remarks));
+    assertEquals(may17(), row.getData()[0]);
+  }
+
+  private static String fieldXml(String value, String formatTag) {
+    return "<transform><fields><field>"
+        + "<name>date</name>"
+        + "<type>Date</type>"
+        + formatTag
+        + "<currency/><decimal/><group/>"
+        + "<nullif>"
+        + value
+        + "</nullif>"
+        + "<length>-1</length><precision>-1</precision>"
+        + "<set_empty_string>N</set_empty_string>"
+        + "</field></fields></transform>";
+  }
+
+  private static ConstantMeta loadMeta(String xml) throws Exception {
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    ConstantMeta meta = new ConstantMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+
+  private static String texts(List<ICheckResult> remarks) {
+    StringBuilder sb = new StringBuilder();
+    for (ICheckResult remark : remarks) {
+      sb.append(remark.getText()).append("; ");
+    }
+    return sb.toString();
+  }
+
+  private static Date may17() {
+    return new GregorianCalendar(2024, Calendar.MAY, 17, 10, 30).getTime();
+  }
+}
diff --git 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
index 7199aec35f..edc5c8d145 100644
--- 
a/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
+++ 
b/plugins/transforms/delete/src/main/java/org/apache/hop/pipeline/transforms/delete/DeleteMeta.java
@@ -107,7 +107,7 @@ public class DeleteMeta extends BaseTransformMeta<Delete, 
DeleteData> {
    */
   public int getCommitSize(IVariables vs) {
     // this happens when the transform is created via API and no setDefaults 
was called
-    commitSize = (commitSize == null) ? "0" : commitSize;
+    commitSize = Const.NVL(commitSize, "0");
     String resolved = vs.resolve(commitSize);
     String expanded = Const.expandIntegerString(resolved);
     return Integer.parseInt(expanded != null ? expanded : resolved);
diff --git 
a/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteBlankCommitSizeTest.java
 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteBlankCommitSizeTest.java
new file mode 100644
index 0000000000..14671b6477
--- /dev/null
+++ 
b/plugins/transforms/delete/src/test/java/org/apache/hop/pipeline/transforms/delete/DeleteBlankCommitSizeTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.delete;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/**
+ * A blank Commit size must fall back to 0 (no batching). Up to 2.17 a saved 
{@code <commit/>}
+ * loaded as null; since 2.18 it loads as an empty string, which must not be 
parsed as an integer
+ * (NumberFormatException at transform start-up).
+ */
+class DeleteBlankCommitSizeTest {
+
+  @Test
+  void blankCommitSizeFallsBackToZero() throws Exception {
+    DeleteMeta meta = loadMeta("<commit/>");
+
+    assertEquals(0, meta.getCommitSize(new Variables()));
+  }
+
+  @Test
+  void specifiedCommitSizeIsUsed() throws Exception {
+    DeleteMeta meta = loadMeta("<commit>100</commit>");
+
+    assertEquals(100, meta.getCommitSize(new Variables()));
+  }
+
+  private static DeleteMeta loadMeta(String commitTag) throws Exception {
+    String xml = "<transform>" + commitTag + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    DeleteMeta meta = new DeleteMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInput.java
 
b/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInput.java
index 593392e7b8..b68711f493 100644
--- 
a/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInput.java
+++ 
b/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInput.java
@@ -830,7 +830,7 @@ public class ExcelInput extends 
BaseTransform<ExcelInputMeta, ExcelInputData> {
   private void initErrorHandling() {
     List<IFileErrorHandler> errorHandlers = new ArrayList<>(2);
 
-    if (meta.getLineNumberFilesDestinationDirectory() != null) {
+    if (!Utils.isEmpty(meta.getLineNumberFilesDestinationDirectory())) {
       errorHandlers.add(
           new FileErrorHandlerContentLineNumber(
               getPipeline().getExecutionStartDate(),
@@ -839,7 +839,7 @@ public class ExcelInput extends 
BaseTransform<ExcelInputMeta, ExcelInputData> {
               "Latin1",
               this));
     }
-    if (meta.getErrorFilesDestinationDirectory() != null) {
+    if (!Utils.isEmpty(meta.getErrorFilesDestinationDirectory())) {
       errorHandlers.add(
           new FileErrorHandlerMissingFiles(
               getPipeline().getExecutionStartDate(),
diff --git 
a/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputBlankErrorFoldersTest.java
 
b/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputBlankErrorFoldersTest.java
new file mode 100644
index 0000000000..d7f74a5c58
--- /dev/null
+++ 
b/plugins/transforms/excel/src/test/java/org/apache/hop/pipeline/transforms/excelinput/ExcelInputBlankErrorFoldersTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.excelinput;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.when;
+
+import java.lang.reflect.Field;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import 
org.apache.hop.pipeline.transform.errorhandling.AbstractFileErrorHandler;
+import 
org.apache.hop.pipeline.transform.errorhandling.CompositeFileErrorHandler;
+import org.apache.hop.pipeline.transform.errorhandling.IFileErrorHandler;
+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.junit.jupiter.api.io.TempDir;
+import org.w3c.dom.Node;
+
+/**
+ * Blank error and line-number file folders must not register file error 
handlers. Up to 2.17 a
+ * saved {@code <error_line_files_destination_directory/>} loaded as null; 
since 2.18 it loads as an
+ * empty string, which would register a handler writing to {@code "" + "/" + 
name}, i.e. the
+ * filesystem root.
+ */
+class ExcelInputBlankErrorFoldersTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<ExcelInputMeta, ExcelInputData> helper;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    helper = new TransformMockHelper<>("Excel input", ExcelInputMeta.class, 
ExcelInputData.class);
+    when(helper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(helper.iLogChannel);
+    
when(helper.logChannelFactory.create(any())).thenReturn(helper.iLogChannel);
+    doReturn(new Date()).when(helper.pipeline).getExecutionStartDate();
+  }
+
+  @AfterEach
+  void tearDown() {
+    helper.cleanUp();
+  }
+
+  @Test
+  void blankFoldersRegisterNoErrorHandlers() throws Exception {
+    ExcelInputMeta meta =
+        loadMeta(
+            "<error_line_files_destination_directory/>"
+                + "<line_number_files_destination_directory/>");
+
+    List<String> directories = initAndGetHandlerDirectories(meta);
+
+    assertTrue(
+        directories.isEmpty(),
+        "No error file handlers expected for blank folders, got directories " 
+ directories);
+  }
+
+  @Test
+  void specifiedFoldersRegisterErrorHandlers(@TempDir Path tempDir) throws 
Exception {
+    String folder = tempDir.toString();
+    ExcelInputMeta meta =
+        loadMeta(
+            "<error_line_files_destination_directory>"
+                + folder
+                + "</error_line_files_destination_directory>"
+                + "<line_number_files_destination_directory>"
+                + folder
+                + "</line_number_files_destination_directory>");
+
+    List<String> directories = initAndGetHandlerDirectories(meta);
+
+    assertEquals(List.of(folder, folder), directories);
+  }
+
+  private List<String> initAndGetHandlerDirectories(ExcelInputMeta meta) 
throws Exception {
+    ExcelInputData data = new ExcelInputData();
+    ExcelInput transform =
+        new ExcelInput(helper.transformMeta, meta, data, 0, 
helper.pipelineMeta, helper.pipeline);
+    // The result of init() is irrelevant here: the error handlers are set up 
first.
+    transform.init();
+
+    Field handlersField = 
CompositeFileErrorHandler.class.getDeclaredField("handlers");
+    handlersField.setAccessible(true);
+    Field directoryField = 
AbstractFileErrorHandler.class.getDeclaredField("destinationDirectory");
+    directoryField.setAccessible(true);
+
+    List<String> directories = new ArrayList<>();
+    @SuppressWarnings("unchecked")
+    List<IFileErrorHandler> handlers =
+        (List<IFileErrorHandler>) handlersField.get(data.errorHandler);
+    for (IFileErrorHandler handler : handlers) {
+      directories.add((String) directoryField.get(handler));
+    }
+    return directories;
+  }
+
+  private static ExcelInputMeta loadMeta(String folderTags) throws Exception {
+    String xml =
+        "<transform>"
+            + "<error_ignored>Y</error_ignored>"
+            + "<bad_line_files_destination_directory/>"
+            + folderTags
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    ExcelInputMeta meta = new ExcelInputMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/insertupdate/src/main/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateMeta.java
 
b/plugins/transforms/insertupdate/src/main/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateMeta.java
index 0dc928948e..d1289c70ca 100644
--- 
a/plugins/transforms/insertupdate/src/main/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateMeta.java
+++ 
b/plugins/transforms/insertupdate/src/main/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateMeta.java
@@ -115,7 +115,7 @@ public class InsertUpdateMeta extends 
BaseTransformMeta<InsertUpdate, InsertUpda
    */
   public int getCommitSizeVar(IVariables vs) {
     // this happens when the transform is created via API and no setDefaults 
was called
-    commitSize = (commitSize == null) ? "0" : commitSize;
+    commitSize = Const.NVL(commitSize, "0");
     String resolved = vs.resolve(commitSize);
     String expanded = Const.expandIntegerString(resolved);
     return Integer.parseInt(expanded != null ? expanded : resolved);
diff --git 
a/plugins/transforms/insertupdate/src/test/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateBlankCommitSizeTest.java
 
b/plugins/transforms/insertupdate/src/test/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateBlankCommitSizeTest.java
new file mode 100644
index 0000000000..84e91f23c9
--- /dev/null
+++ 
b/plugins/transforms/insertupdate/src/test/java/org/apache/hop/pipeline/transforms/insertupdate/InsertUpdateBlankCommitSizeTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.insertupdate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/**
+ * A blank Commit size must fall back to 0 (no batching). Up to 2.17 a saved 
{@code <commit/>}
+ * loaded as null; since 2.18 it loads as an empty string, which must not be 
parsed as an integer
+ * (NumberFormatException at transform start-up).
+ */
+class InsertUpdateBlankCommitSizeTest {
+
+  @Test
+  void blankCommitSizeFallsBackToZero() throws Exception {
+    InsertUpdateMeta meta = loadMeta("<commit/>");
+
+    assertEquals(0, meta.getCommitSizeVar(new Variables()));
+  }
+
+  @Test
+  void specifiedCommitSizeIsUsed() throws Exception {
+    InsertUpdateMeta meta = loadMeta("<commit>100</commit>");
+
+    assertEquals(100, meta.getCommitSizeVar(new Variables()));
+  }
+
+  private static InsertUpdateMeta loadMeta(String commitTag) throws Exception {
+    String xml = "<transform>" + commitTag + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    InsertUpdateMeta meta = new InsertUpdateMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/jdbc-metadata/src/main/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataMeta.java
 
b/plugins/transforms/jdbc-metadata/src/main/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataMeta.java
index 539d6c0441..45eada6a24 100644
--- 
a/plugins/transforms/jdbc-metadata/src/main/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataMeta.java
+++ 
b/plugins/transforms/jdbc-metadata/src/main/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataMeta.java
@@ -32,6 +32,7 @@ import org.apache.hop.core.row.value.ValueMetaBoolean;
 import org.apache.hop.core.row.value.ValueMetaFactory;
 import org.apache.hop.core.row.value.ValueMetaInteger;
 import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.HopMetadataPropertyType;
@@ -620,7 +621,8 @@ public class JdbcMetadataMeta extends 
BaseTransformMeta<JdbcMetadata, JdbcMetada
         if (!fieldName.equals(outputField.getName())) {
           continue;
         }
-        String newFieldName = outputField.getRename() == null ? fieldName : 
outputField.getRename();
+        String newFieldName =
+            Utils.isEmpty(outputField.getRename()) ? fieldName : 
outputField.getRename();
         try {
           field = ValueMetaFactory.createValueMeta(newFieldName, 
field.getType());
           field.setOrigin(origin);
diff --git 
a/plugins/transforms/jdbc-metadata/src/test/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataBlankRenameTest.java
 
b/plugins/transforms/jdbc-metadata/src/test/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataBlankRenameTest.java
new file mode 100644
index 0000000000..659096e2d6
--- /dev/null
+++ 
b/plugins/transforms/jdbc-metadata/src/test/java/org/apache/hop/pipeline/transforms/jdbcmetadata/JdbcMetadataBlankRenameTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.jdbcmetadata;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/**
+ * An output field with a blank "Rename to" must keep the JDBC metadata column 
name. Up to 2.17 a
+ * saved {@code <rename/>} loaded as null; since 2.18 it loads as an empty 
string, which must not
+ * produce output fields without a name.
+ */
+class JdbcMetadataBlankRenameTest {
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopClientEnvironment.init();
+  }
+
+  @Test
+  void blankRenameKeepsOriginalFieldName() throws Exception {
+    JdbcMetadataMeta meta = loadMeta("<rename/>", "<rename/>");
+
+    IRowMeta row = new RowMeta();
+    meta.getFields(row, "JDBC metadata", null, null, new Variables(), null);
+
+    assertArrayEquals(new String[] {"TABLE_NAME", "REMARKS"}, 
row.getFieldNames());
+  }
+
+  @Test
+  void specifiedRenameIsApplied() throws Exception {
+    JdbcMetadataMeta meta = loadMeta("<rename>tableName</rename>", 
"<rename>comment</rename>");
+
+    IRowMeta row = new RowMeta();
+    meta.getFields(row, "JDBC metadata", null, null, new Variables(), null);
+
+    assertArrayEquals(new String[] {"tableName", "comment"}, 
row.getFieldNames());
+  }
+
+  private static JdbcMetadataMeta loadMeta(String tableNameRename, String 
remarksRename)
+      throws Exception {
+    String xml =
+        "<transform>"
+            + "<connection/>"
+            + "<methodName>getTables</methodName>"
+            + "<alwaysPassInputRow>N</alwaysPassInputRow>"
+            + "<argumentSourceFields>N</argumentSourceFields>"
+            + "<removeArgumentFields>N</removeArgumentFields>"
+            + "<arguments/>"
+            + "<outputFields>"
+            + "<outputField><name>TABLE_NAME</name>"
+            + tableNameRename
+            + "</outputField>"
+            + "<outputField><name>REMARKS</name>"
+            + remarksRename
+            + "</outputField>"
+            + "</outputFields>"
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    JdbcMetadataMeta meta = new JdbcMetadataMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/update/src/main/java/org/apache/hop/pipeline/transforms/update/UpdateMeta.java
 
b/plugins/transforms/update/src/main/java/org/apache/hop/pipeline/transforms/update/UpdateMeta.java
index 79ae398ae4..335e1f4b41 100644
--- 
a/plugins/transforms/update/src/main/java/org/apache/hop/pipeline/transforms/update/UpdateMeta.java
+++ 
b/plugins/transforms/update/src/main/java/org/apache/hop/pipeline/transforms/update/UpdateMeta.java
@@ -127,7 +127,7 @@ public class UpdateMeta extends BaseTransformMeta<Update, 
UpdateData> {
    */
   public int getCommitSize(IVariables vs) {
     // this happens when the transform is created via API and no setDefaults 
was called
-    commitSize = (commitSize == null) ? "0" : commitSize;
+    commitSize = Const.NVL(commitSize, "0");
     String resolved = vs.resolve(commitSize);
     String expanded = Const.expandIntegerString(resolved);
     return Integer.parseInt(expanded != null ? expanded : resolved);
diff --git 
a/plugins/transforms/update/src/test/java/org/apache/hop/pipeline/transforms/update/UpdateBlankCommitSizeTest.java
 
b/plugins/transforms/update/src/test/java/org/apache/hop/pipeline/transforms/update/UpdateBlankCommitSizeTest.java
new file mode 100644
index 0000000000..7a8cb281de
--- /dev/null
+++ 
b/plugins/transforms/update/src/test/java/org/apache/hop/pipeline/transforms/update/UpdateBlankCommitSizeTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.update;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/**
+ * A blank Commit size must fall back to 0 (no batching). Up to 2.17 a saved 
{@code <commit/>}
+ * loaded as null; since 2.18 it loads as an empty string, which must not be 
parsed as an integer
+ * (NumberFormatException at transform start-up).
+ */
+class UpdateBlankCommitSizeTest {
+
+  @Test
+  void blankCommitSizeFallsBackToZero() throws Exception {
+    UpdateMeta meta = loadMeta("<commit/>");
+
+    assertEquals(0, meta.getCommitSize(new Variables()));
+  }
+
+  @Test
+  void specifiedCommitSizeIsUsed() throws Exception {
+    UpdateMeta meta = loadMeta("<commit>100</commit>");
+
+    assertEquals(100, meta.getCommitSize(new Variables()));
+  }
+
+  private static UpdateMeta loadMeta(String commitTag) throws Exception {
+    String xml = "<transform>" + commitTag + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    UpdateMeta meta = new UpdateMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
index 2cfa767551..35be802f29 100644
--- 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
+++ 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
@@ -152,11 +152,11 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
   private int maxLengthOfMappedStringValues() {
     int maxlen = -1;
     for (Values v : this.values) {
-      if (v.getTarget() != null && v.getTarget().length() > maxlen) {
+      if (!Utils.isEmpty(v.getTarget()) && v.getTarget().length() > maxlen) {
         maxlen = v.getTarget().length();
       }
     }
-    if (nonMatchDefault != null && nonMatchDefault.length() > maxlen) {
+    if (!Utils.isEmpty(nonMatchDefault) && nonMatchDefault.length() > maxlen) {
       maxlen = nonMatchDefault.length();
     }
     return maxlen;
diff --git 
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperBlankTargetLengthTest.java
 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperBlankTargetLengthTest.java
new file mode 100644
index 0000000000..6b554bc927
--- /dev/null
+++ 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperBlankTargetLengthTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.valuemapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.w3c.dom.Node;
+
+/**
+ * A new String target field whose mapped targets and non-match default are 
all blank has an
+ * undefined length (-1). Up to 2.17 a saved {@code <target_value/>} or {@code 
<non_match_default/>}
+ * loaded as null; since 2.18 it loads as an empty string, which must not size 
the field to 0.
+ */
+class ValueMapperBlankTargetLengthTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void blankTargetsAndDefaultLeaveLengthUndefined() throws Exception {
+    ValueMapperMeta meta = loadMeta("<non_match_default/>", "<target_value/>", 
"<target_value/>");
+
+    assertEquals(-1, targetLength(meta));
+  }
+
+  @Test
+  void nonBlankTargetsSizeFieldToLongestValue() throws Exception {
+    ValueMapperMeta meta =
+        loadMeta(
+            "<non_match_default>n/a</non_match_default>",
+            "<target_value>Belgium</target_value>",
+            "<target_value>Netherlands</target_value>");
+
+    assertEquals("Netherlands".length(), targetLength(meta));
+  }
+
+  private static int targetLength(ValueMapperMeta meta) {
+    IRowMeta row = new RowMeta();
+    row.addValueMeta(new ValueMetaString("code"));
+    meta.getFields(row, "mapper", null, new TransformMeta(), new Variables(), 
null);
+    IValueMeta target = row.searchValueMeta("country");
+    assertNotNull(target, "Target field was not added");
+    return target.getLength();
+  }
+
+  private static ValueMapperMeta loadMeta(String defaultTag, String target1, 
String target2)
+      throws Exception {
+    String xml =
+        "<transform>"
+            + "<field_to_use>code</field_to_use>"
+            + "<target_field>country</target_field>"
+            + defaultTag
+            + "<target_type>String</target_type>"
+            + "<fields>"
+            + "<field><source_value>BE</source_value>"
+            + target1
+            + "</field>"
+            + "<field><source_value>NL</source_value>"
+            + target2
+            + "</field>"
+            + "</fields>"
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
index 5789dd09c6..d6bcec43ca 100644
--- 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
+++ 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
@@ -274,13 +274,13 @@ public class WebService extends 
BaseTransform<WebServiceMeta, WebServiceData> {
     xml.append("    <" + NS_PREFIX + ":")
         .append(requestOperation)
         .append(">\n"); // OPEN request operation
-    if (meta.getInFieldContainerName() != null) {
+    if (!Utils.isEmpty(meta.getInFieldContainerName())) {
       xml.append("      <" + NS_PREFIX + ":" + meta.getInFieldContainerName() 
+ ">\n");
     }
 
     addParametersToXML(xml, bodyNames, qualifyWSField);
 
-    if (meta.getInFieldContainerName() != null) {
+    if (!Utils.isEmpty(meta.getInFieldContainerName())) {
       xml.append("      </" + NS_PREFIX + ":" + meta.getInFieldContainerName() 
+ ">\n");
     }
     xml.append("    </" + NS_PREFIX + ":")
@@ -306,7 +306,7 @@ public class WebService extends 
BaseTransform<WebServiceMeta, WebServiceData> {
     //
     for (Object[] vCurrentRow : data.argumentRows) {
 
-      if (meta.getInFieldArgumentName() != null) {
+      if (!Utils.isEmpty(meta.getInFieldArgumentName())) {
         xml.append("        <")
             .append(NS_PREFIX)
             .append(":")
@@ -364,7 +364,7 @@ public class WebService extends 
BaseTransform<WebServiceMeta, WebServiceData> {
           }
         }
       }
-      if (meta.getInFieldArgumentName() != null) {
+      if (!Utils.isEmpty(meta.getInFieldArgumentName())) {
         xml.append("        </")
             .append(NS_PREFIX)
             .append(":")
@@ -1016,10 +1016,10 @@ public class WebService extends 
BaseTransform<WebServiceMeta, WebServiceData> {
             }
             // If we end the xml element named as the return type, we
             // finish a row
-            if ((meta.getOutFieldArgumentName() == null
+            if ((Utils.isEmpty(meta.getOutFieldArgumentName())
                 && meta.getOperationName().equals(vReader.getLocalName()))) {
               oneValueRowProcessing = false;
-            } else if (meta.getOutFieldArgumentName() != null
+            } else if (!Utils.isEmpty(meta.getOutFieldArgumentName())
                 && 
meta.getOutFieldArgumentName().equals(vReader.getLocalName())) {
               putRow(data.outputRowMeta, outputRowData);
               processing = false;
diff --git 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceDialog.java
 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceDialog.java
index 686583f9db..723494a292 100644
--- 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceDialog.java
+++ 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceDialog.java
@@ -668,8 +668,8 @@ public class WebServiceDialog extends BaseTransformDialog {
       wOperation.setText(meta.getOperationName() == null ? "" : 
meta.getOperationName());
     }
     wOperationRequest.setText(Const.NVL(meta.getOperationRequestName(), ""));
-    if (meta.getInFieldContainerName() != null
-        || meta.getInFieldArgumentName() != null
+    if (!Utils.isEmpty(meta.getInFieldContainerName())
+        || !Utils.isEmpty(meta.getInFieldArgumentName())
         || !meta.getFieldsIn().isEmpty()) {
       addTabFieldIn();
 
diff --git 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceMeta.java
 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceMeta.java
index 30961c55b2..8b79bcfdd7 100644
--- 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceMeta.java
+++ 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebServiceMeta.java
@@ -213,7 +213,8 @@ public class WebServiceMeta extends 
BaseTransformMeta<WebService, WebServiceData
               "Transform is receiving info from other transforms.",
               transformMeta);
       remarks.add(cr);
-    } else if (getInFieldArgumentName() != null || getInFieldContainerName() 
!= null) {
+    } else if (!Utils.isEmpty(getInFieldArgumentName())
+        || !Utils.isEmpty(getInFieldContainerName())) {
       cr =
           new CheckResult(
               ICheckResult.TYPE_RESULT_ERROR,
diff --git 
a/plugins/transforms/webservices/src/test/java/org/apache/hop/pipeline/transforms/webservices/WebServiceBlankContainerRequestTest.java
 
b/plugins/transforms/webservices/src/test/java/org/apache/hop/pipeline/transforms/webservices/WebServiceBlankContainerRequestTest.java
new file mode 100644
index 0000000000..e243360be0
--- /dev/null
+++ 
b/plugins/transforms/webservices/src/test/java/org/apache/hop/pipeline/transforms/webservices/WebServiceBlankContainerRequestTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.webservices;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.apache.hop.pipeline.transforms.webservices.wsdl.WsdlOpParameterList;
+import org.apache.hop.pipeline.transforms.webservices.wsdl.WsdlOperation;
+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.w3c.dom.Node;
+
+/**
+ * A blank "In field container" or "In field argument" must not produce an 
element in the SOAP
+ * request. Up to 2.17 a saved {@code <wsInFieldContainer/>} loaded as null; 
since 2.18 it loads as
+ * an empty string, which must not turn into a malformed {@code <ns:>} element.
+ */
+class WebServiceBlankContainerRequestTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<WebServiceMeta, WebServiceData> mockHelper;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    mockHelper =
+        new TransformMockHelper<>("WebService", WebServiceMeta.class, 
WebServiceData.class);
+    when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(mockHelper.iLogChannel);
+  }
+
+  @AfterEach
+  void cleanUp() {
+    mockHelper.cleanUp();
+  }
+
+  @Test
+  void blankContainerAndArgumentAddNoElement() throws Exception {
+    String request = requestXml("<wsInFieldContainer/><wsInFieldArgument/>");
+
+    assertFalse(request.contains("<ns:>"), request);
+    assertFalse(request.contains("</ns:>"), request);
+    assertDoesNotThrow(() -> XmlHandler.loadXmlString(request), request);
+  }
+
+  @Test
+  void specifiedContainerAndArgumentAddTheirElements() throws Exception {
+    String request =
+        requestXml(
+            "<wsInFieldContainer>Books</wsInFieldContainer>"
+                + "<wsInFieldArgument>Book</wsInFieldArgument>");
+
+    assertTrue(request.contains("<ns:Books>") && 
request.contains("</ns:Books>"), request);
+    assertTrue(request.contains("<ns:Book>") && 
request.contains("</ns:Book>"), request);
+    assertDoesNotThrow(() -> XmlHandler.loadXmlString(request), request);
+  }
+
+  /**
+   * Load the transform from XML, the way it is saved in 0013-webservice.hpl, 
and build a request.
+   */
+  private String requestXml(String inFieldTags) throws Exception {
+    String xml =
+        "<transform>"
+            + "<wsURL>http://localhost/BookService.wsdl</wsURL>"
+            + "<wsOperation>AddBook</wsOperation>"
+            + "<wsOperationRequest/>"
+            + 
"<wsOperationNamespace>http://www.cleverbuilder.com/BookService/</wsOperationNamespace>"
+            + inFieldTags
+            + "<wsOutFieldContainer>Book</wsOutFieldContainer>"
+            + "<wsOutFieldArgument>Book</wsOutFieldArgument>"
+            + "<callTransform>1</callTransform>"
+            + "<passingInputData>Y</passingInputData>"
+            + "<compatible>N</compatible>"
+            + "<repeating_element/>"
+            + "<reply_as_string>Y</reply_as_string>"
+            + "<fieldsIn/><fieldsOut/>"
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    WebServiceMeta meta = new WebServiceMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+
+    WebServiceData data = new WebServiceData();
+    data.argumentRows.add(new Object[0]);
+    WebService transform =
+        new WebService(
+            mockHelper.transformMeta, meta, data, 0, mockHelper.pipelineMeta, 
mockHelper.pipeline);
+    Field indexList = WebService.class.getDeclaredField("indexList");
+    indexList.setAccessible(true);
+    indexList.set(transform, new ArrayList<Integer>());
+
+    WsdlOpParameterList parameters = mock(WsdlOpParameterList.class);
+    when(parameters.iterator()).thenReturn(Collections.emptyIterator());
+    when(parameters.getHeaderNames()).thenReturn(new HashSet<>());
+    WsdlOperation operation = mock(WsdlOperation.class);
+    when(operation.getParameters()).thenReturn(parameters);
+
+    Method getRequestXml =
+        WebService.class.getDeclaredMethod("getRequestXML", 
WsdlOperation.class, boolean.class);
+    getRequestXml.setAccessible(true);
+    return (String) getRequestXml.invoke(transform, operation, true);
+  }
+}
diff --git 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
index b0434dfd15..8bc4bad5f6 100644
--- 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
+++ 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXml.java
@@ -350,7 +350,7 @@ public class AddXml extends BaseTransform<AddXmlMeta, 
AddXmlData> {
               .newDocumentBuilder()
               .getDOMImplementation());
 
-      if (meta.getEncoding() != null) {
+      if (!Utils.isEmpty(meta.getEncoding())) {
         getSerializer().setOutputProperty(OutputKeys.ENCODING, 
meta.getEncoding());
       }
 
diff --git 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoin.java
 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoin.java
index d1518bb667..4a192fafdb 100644
--- 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoin.java
+++ 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoin.java
@@ -32,6 +32,7 @@ import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopXmlException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.core.xml.XmlParserFactoryProducer;
 import org.apache.hop.i18n.BaseMessages;
@@ -269,7 +270,7 @@ public class XmlJoin extends BaseTransform<XmlJoinMeta, 
XmlJoinData> {
     try {
       Transformer transformer = 
XmlHandler.createSecureTransformerFactory().newTransformer();
 
-      if (meta.getEncoding() != null) {
+      if (!Utils.isEmpty(meta.getEncoding())) {
         transformer.setOutputProperty(OutputKeys.ENCODING, meta.getEncoding());
       }
 
diff --git 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlBlankEncodingTest.java
 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlBlankEncodingTest.java
new file mode 100644
index 0000000000..23a66030ad
--- /dev/null
+++ 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/addxml/AddXmlBlankEncodingTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.xml.addxml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.when;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+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.ValueMetaString;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transform.RowAdapter;
+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.w3c.dom.Node;
+
+/**
+ * Add XML with a blank Encoding must produce the same XML as up to 2.17, 
where a saved {@code
+ * <encoding/>} loaded as null and the serializer kept its default (UTF-8) 
encoding. Since 2.18 the
+ * tag loads as an empty string.
+ */
+class AddXmlBlankEncodingTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<AddXmlMeta, AddXmlData> tmh;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    tmh = new TransformMockHelper<>("AddXml", AddXmlMeta.class, 
AddXmlData.class);
+    doReturn(tmh.iLogChannel).when(tmh.logChannelFactory).create(any(), 
any(ILoggingObject.class));
+    when(tmh.pipeline.isRunning()).thenReturn(true);
+  }
+
+  @AfterEach
+  void tearDown() {
+    tmh.cleanUp();
+  }
+
+  @Test
+  void blankEncodingProducesDefaultUtf8Declaration() throws Exception {
+    String xml = runAddXml("<encoding/>");
+
+    assertEquals("<?xml version=\"1.0\" 
encoding=\"UTF-8\"?><Row><name>café</name></Row>", xml);
+    assertTrue(XmlHandler.loadXmlString(xml) != null);
+  }
+
+  @Test
+  void specifiedEncodingIsWrittenInDeclaration() throws Exception {
+    String xml = runAddXml("<encoding>ISO-8859-1</encoding>");
+
+    assertEquals(
+        "<?xml version=\"1.0\" 
encoding=\"ISO-8859-1\"?><Row><name>café</name></Row>", xml);
+  }
+
+  private String runAddXml(String encodingTag) throws Exception {
+    AddXmlMeta meta = loadMeta(encodingTag);
+    AddXml addXml =
+        new AddXml(tmh.transformMeta, meta, new AddXmlData(), 0, 
tmh.pipelineMeta, tmh.pipeline);
+    assertTrue(addXml.init(), "init failed");
+
+    IRowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaString("name"));
+    IRowSet rowSet = tmh.getMockInputRowSet(new Object[][] {{"café"}});
+    when(rowSet.getRowMeta()).thenReturn(inputRowMeta);
+    addXml.setInputRowSets(new ArrayList<>(Collections.singletonList(rowSet)));
+
+    List<Object[]> written = new ArrayList<>();
+    addXml.addRowListener(
+        new RowAdapter() {
+          @Override
+          public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+            written.add(row);
+          }
+        });
+
+    assertTrue(addXml.processRow());
+    assertEquals(0, addXml.getErrors());
+    assertEquals(1, written.size());
+    return (String) written.get(0)[1];
+  }
+
+  private static AddXmlMeta loadMeta(String encodingTag) throws Exception {
+    String xml =
+        "<transform>"
+            + encodingTag
+            + "<valueName>xml</valueName>"
+            + "<xml_repeat_element>Row</xml_repeat_element>"
+            + 
"<file><omitXMLheader>N</omitXMLheader><omitNullValues>N</omitNullValues></file>"
+            + "<fields><field>"
+            + "<name>name</name><element>name</element><type>String</type>"
+            + "<format/><currency/><decimal/><group/><nullif/>"
+            + "<length>-1</length><precision>-1</precision>"
+            + "<attribute>N</attribute><attributeParentName/>"
+            + "</field></fields>"
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    AddXmlMeta meta = new AddXmlMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoinBlankEncodingTest.java
 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoinBlankEncodingTest.java
new file mode 100644
index 0000000000..532f48c244
--- /dev/null
+++ 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmljoin/XmlJoinBlankEncodingTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.xml.xmljoin;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+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.ValueMetaString;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transform.RowAdapter;
+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.w3c.dom.Node;
+
+/**
+ * XML Join with a blank Encoding must produce the same XML as up to 2.17, 
where a saved {@code
+ * <encoding/>} loaded as null and the serializer kept its default (UTF-8) 
encoding. Since 2.18 the
+ * tag loads as an empty string.
+ */
+class XmlJoinBlankEncodingTest {
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<XmlJoinMeta, XmlJoinData> tmh;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    tmh = new TransformMockHelper<>("XmlJoin", XmlJoinMeta.class, 
XmlJoinData.class);
+    doReturn(tmh.iLogChannel).when(tmh.logChannelFactory).create(any(), 
any(ILoggingObject.class));
+    when(tmh.pipeline.isRunning()).thenReturn(true);
+  }
+
+  @AfterEach
+  void tearDown() {
+    tmh.cleanUp();
+  }
+
+  @Test
+  void blankEncodingProducesDefaultUtf8Declaration() throws Exception {
+    String xml = runXmlJoin("<encoding/>");
+
+    assertEquals("<?xml version=\"1.0\" 
encoding=\"UTF-8\"?><root><child>café</child></root>", xml);
+    assertTrue(XmlHandler.loadXmlString(xml) != null);
+  }
+
+  @Test
+  void specifiedEncodingIsWrittenInDeclaration() throws Exception {
+    String xml = runXmlJoin("<encoding>ISO-8859-1</encoding>");
+
+    assertEquals(
+        "<?xml version=\"1.0\" 
encoding=\"ISO-8859-1\"?><root><child>café</child></root>", xml);
+  }
+
+  private String runXmlJoin(String encodingTag) throws Exception {
+    XmlJoinMeta meta = loadMeta(encodingTag);
+    XmlJoin xmlJoin =
+        spy(
+            new XmlJoin(
+                tmh.transformMeta, meta, new XmlJoinData(), 0, 
tmh.pipelineMeta, tmh.pipeline));
+
+    IRowSet targetRowSet = rowSet("targetField", "<root/>");
+    IRowSet sourceRowSet = rowSet("sourceField", "<child>café</child>");
+    List<IRowSet> inputRowSets = new ArrayList<>();
+    inputRowSets.add(sourceRowSet);
+    inputRowSets.add(targetRowSet);
+    xmlJoin.setInputRowSets(inputRowSets);
+    doReturn(targetRowSet).when(xmlJoin).findInputRowSet("target");
+    doReturn(sourceRowSet).when(xmlJoin).findInputRowSet("source");
+
+    assertTrue(xmlJoin.init(), "init failed");
+
+    List<Object[]> written = new ArrayList<>();
+    xmlJoin.addRowListener(
+        new RowAdapter() {
+          @Override
+          public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+            written.add(row);
+          }
+        });
+
+    assertTrue(xmlJoin.processRow());
+    assertFalse(xmlJoin.processRow());
+    assertEquals(1, written.size());
+    return (String) written.get(0)[1];
+  }
+
+  private IRowSet rowSet(String fieldName, String value) {
+    IRowSet rowSet = tmh.getMockInputRowSet(new Object[][] {{value}});
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString(fieldName));
+    when(rowSet.getRowMeta()).thenReturn(rowMeta);
+    return rowSet;
+  }
+
+  private static XmlJoinMeta loadMeta(String encodingTag) throws Exception {
+    String xml =
+        "<transform>"
+            + "<complexJoin>N</complexJoin>"
+            + "<targetXmlTransform>target</targetXmlTransform>"
+            + "<targetXmlField>targetField</targetXmlField>"
+            + "<sourceXmlTransform>source</sourceXmlTransform>"
+            + "<sourceXmlField>sourceField</sourceXmlField>"
+            + "<valueXmlField>result</valueXmlField>"
+            + "<targetXPath>/root</targetXPath>"
+            + "<joinCompareField/>"
+            + encodingTag
+            + "<omitXmlHeader>N</omitXmlHeader>"
+            + "<omitNullValues>N</omitNullValues>"
+            + "</transform>";
+    Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), 
"transform");
+    XmlJoinMeta meta = new XmlJoinMeta();
+    meta.loadXml(transformNode, new MemoryMetadataProvider());
+    return meta;
+  }
+}

Reply via email to