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