This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new c54a59c045 Fix put ftp does not handle password error #5894 (#5900)
c54a59c045 is described below
commit c54a59c045851fe49956068278cd0d167c840f3c
Author: lance <[email protected]>
AuthorDate: Mon Nov 17 20:43:55 2025 +0800
Fix put ftp does not handle password error #5894 (#5900)
* fix put ftp does not handle password error
Signed-off-by: lance <[email protected]>
* Add integration test
---------
Signed-off-by: lance <[email protected]>
Co-authored-by: Hans Van Akelyen <[email protected]>
---
.../integration-tests/integration-tests-ftp.yaml | 34 +
integration-tests/ftp/dev-env-config.json | 19 +
integration-tests/ftp/files/test.txt | 1 +
integration-tests/ftp/hop-config.json | 290 +++++++
integration-tests/ftp/main-001-ftp-put.hwf | 96 +++
.../metadata/pipeline-run-configuration/local.json | 22 +
.../metadata/workflow-run-configuration/local.json | 11 +
integration-tests/ftp/project-config.json | 13 +
plugins/actions/ftp/pom.xml | 2 +-
.../hop/workflow/actions/ftpput/ActionFtpPut.java | 842 +++++++--------------
.../actions/ftpput/ActionFtpPutDialog.java | 27 +-
.../hop/workflow/actions/util/FtpHelper.java | 56 ++
.../workflow/actions/ftpput/ActionFtpPutTests.java | 237 ++++++
.../ftpput/WorkflowActionFtpPutLoadSaveTest.java | 2 +-
.../hop/workflow/actions/util/FtpHelperTests.java | 91 +++
.../transforms/addsequence/AddSequenceMeta.java | 32 +-
.../addsequence/AddSequenceMetaTest.java | 6 +-
.../addsnowflakeid/AddSnowflakeIdData.java | 1 +
.../addsnowflakeid/SnowflakeSafeIdGenerator.java | 2 +
19 files changed, 1179 insertions(+), 605 deletions(-)
diff --git a/docker/integration-tests/integration-tests-ftp.yaml
b/docker/integration-tests/integration-tests-ftp.yaml
new file mode 100644
index 0000000000..18191884f3
--- /dev/null
+++ b/docker/integration-tests/integration-tests-ftp.yaml
@@ -0,0 +1,34 @@
+# 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.
+
+services:
+ integration_test_xml:
+ extends:
+ file: integration-tests-base.yaml
+ service: integration_test
+ links:
+ - ftp-server
+
+ ftp-server:
+ image: "delfer/alpine-ftp-server:latest"
+ hostname: ftp-server
+ ports:
+ - "21:21"
+ - "21000-21010:21000-21010"
+ environment:
+ - "USERS=admin|admin"
+ - "ADDRESS=ftp-server"
diff --git a/integration-tests/ftp/dev-env-config.json
b/integration-tests/ftp/dev-env-config.json
new file mode 100644
index 0000000000..08f3788e4d
--- /dev/null
+++ b/integration-tests/ftp/dev-env-config.json
@@ -0,0 +1,19 @@
+{
+ "variables" : [
+ {
+ "name" : "FTP_SERVER",
+ "value" : "ftp-server",
+ "description" : ""
+ },
+ {
+ "name" : "FTP_USER",
+ "value" : "admin",
+ "description" : ""
+ },
+ {
+ "name" : "FTP_PASSWORD",
+ "value" : "admin",
+ "description" : ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/integration-tests/ftp/files/test.txt
b/integration-tests/ftp/files/test.txt
new file mode 100644
index 0000000000..9f4b6d8bfe
--- /dev/null
+++ b/integration-tests/ftp/files/test.txt
@@ -0,0 +1 @@
+This is a test file
diff --git a/integration-tests/ftp/hop-config.json
b/integration-tests/ftp/hop-config.json
new file mode 100644
index 0000000000..102ac981d5
--- /dev/null
+++ b/integration-tests/ftp/hop-config.json
@@ -0,0 +1,290 @@
+{
+ "variables": [
+ {
+ "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION",
+ "value": "N",
+ "description": "System wide flag to allow lenient string to number
conversion for backward compatibility. If this setting is set to \"Y\", an
string starting with digits will be converted successfully into a number.
(example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending
on the decimal and grouping symbol). The default (N) will be to throw an error
if non-numeric symbols are found in the string."
+ },
+ {
+ "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE",
+ "value": "N",
+ "description": "System wide flag to ignore timezone while writing
date/timestamp value to the database."
+ },
+ {
+ "name": "HOP_LOG_SIZE_LIMIT",
+ "value": "0",
+ "description": "The log size limit for all pipelines and workflows that
don't have the \"log size limit\" property set in their respective properties."
+ },
+ {
+ "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL",
+ "value": "N",
+ "description": "NULL vs Empty String. If this setting is set to Y, an
empty string and null are different. Otherwise they are not."
+ },
+ {
+ "name": "HOP_MAX_LOG_SIZE_IN_LINES",
+ "value": "0",
+ "description": "The maximum number of log lines that are kept internally
by Hop. Set to 0 to keep all rows (default)"
+ },
+ {
+ "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES",
+ "value": "1440",
+ "description": "The maximum age (in minutes) of a log line while being
kept internally by Hop. Set to 0 to keep all rows indefinitely (default)"
+ },
+ {
+ "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE",
+ "value": "5000",
+ "description": "The maximum number of workflow trackers kept in memory"
+ },
+ {
+ "name": "HOP_MAX_ACTIONS_LOGGED",
+ "value": "5000",
+ "description": "The maximum number of action results kept in memory for
logging purposes."
+ },
+ {
+ "name": "HOP_MAX_LOGGING_REGISTRY_SIZE",
+ "value": "10000",
+ "description": "The maximum number of logging registry entries kept in
memory for logging purposes."
+ },
+ {
+ "name": "HOP_LOG_TAB_REFRESH_DELAY",
+ "value": "1000",
+ "description": "The hop log tab refresh delay."
+ },
+ {
+ "name": "HOP_LOG_TAB_REFRESH_PERIOD",
+ "value": "1000",
+ "description": "The hop log tab refresh period."
+ },
+ {
+ "name": "HOP_PLUGIN_CLASSES",
+ "value": null,
+ "description": "A comma delimited list of classes to scan for plugin
annotations"
+ },
+ {
+ "name": "HOP_PLUGIN_PACKAGES",
+ "value": null,
+ "description": "A comma delimited list of packages to scan for plugin
annotations (warning: slow!!)"
+ },
+ {
+ "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT",
+ "value": "0",
+ "description": "The maximum number of transform performance snapshots to
keep in memory. Set to 0 to keep all snapshots indefinitely (default)"
+ },
+ {
+ "name": "HOP_ROWSET_GET_TIMEOUT",
+ "value": "50",
+ "description": "The name of the variable that optionally contains an
alternative rowset get timeout (in ms). This only makes a difference for
extremely short lived pipelines."
+ },
+ {
+ "name": "HOP_ROWSET_PUT_TIMEOUT",
+ "value": "50",
+ "description": "The name of the variable that optionally contains an
alternative rowset put timeout (in ms). This only makes a difference for
extremely short lived pipelines."
+ },
+ {
+ "name": "HOP_CORE_TRANSFORMS_FILE",
+ "value": null,
+ "description": "The name of the project variable that will contain the
alternative location of the hop-transforms.xml file. You can use this to
customize the list of available internal transforms outside of the codebase."
+ },
+ {
+ "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE",
+ "value": null,
+ "description": "The name of the project variable that will contain the
alternative location of the hop-workflow-actions.xml file."
+ },
+ {
+ "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES",
+ "value": "1440",
+ "description": "This project variable will set a time-out after which
waiting, completed or stopped pipelines and workflows will be automatically
cleaned up. The default value is 1440 (one day)."
+ },
+ {
+ "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE",
+ "value": null,
+ "description": "Set this variable to an integer that will be returned as
the Pan JVM exit code."
+ },
+ {
+ "name": "HOP_DISABLE_CONSOLE_LOGGING",
+ "value": "N",
+ "description": "Set this variable to Y to disable standard Hop logging
to the console. (stdout)"
+ },
+ {
+ "name": "HOP_REDIRECT_STDERR",
+ "value": "N",
+ "description": "Set this variable to Y to redirect stderr to Hop
logging."
+ },
+ {
+ "name": "HOP_REDIRECT_STDOUT",
+ "value": "N",
+ "description": "Set this variable to Y to redirect stdout to Hop
logging."
+ },
+ {
+ "name": "HOP_DEFAULT_NUMBER_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default number format"
+ },
+ {
+ "name": "HOP_DEFAULT_BIGNUMBER_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default bignumber format"
+ },
+ {
+ "name": "HOP_DEFAULT_INTEGER_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default integer format"
+ },
+ {
+ "name": "HOP_DEFAULT_DATE_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default date format"
+ },
+ {
+ "name": "HOP_DEFAULT_TIMESTAMP_FORMAT",
+ "value": null,
+ "description": "The name of the variable containing an alternative
default timestamp format"
+ },
+ {
+ "name": "HOP_DEFAULT_SERVLET_ENCODING",
+ "value": null,
+ "description": "Defines the default encoding for servlets, leave it
empty to use Java default encoding"
+ },
+ {
+ "name": "HOP_FAIL_ON_LOGGING_ERROR",
+ "value": "N",
+ "description": "Set this variable to Y when you want the
workflow/pipeline fail with an error when the related logging process (e.g. to
a database) fails."
+ },
+ {
+ "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED",
+ "value": "N",
+ "description": "Set this variable to Y to set the minimum to NULL if
NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN
aggregate and MIN is set to the minimum value that is not NULL. See also the
variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO."
+ },
+ {
+ "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO",
+ "value": "N",
+ "description": "Set this variable to Y to return 0 when all values
within an aggregate are NULL. Otherwise by default a NULL is returned when all
values are NULL."
+ },
+ {
+ "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER",
+ "value": "N",
+ "description": "Set this variable to Y for backward compatibility for
the Text File Output transform. Setting this to Ywill add no header row at all
when the append option is enabled, regardless if the file is existing or not."
+ },
+ {
+ "name": "HOP_PASSWORD_ENCODER_PLUGIN",
+ "value": "Hop",
+ "description": "Specifies the password encoder plugin to use by ID (Hop
is the default)."
+ },
+ {
+ "name": "HOP_SYSTEM_HOSTNAME",
+ "value": null,
+ "description": "You can use this variable to speed up hostname lookup.
Hostname lookup is performed by Hop so that it is capable of logging the server
on which a workflow or pipeline is executed."
+ },
+ {
+ "name": "HOP_SERVER_JETTY_ACCEPTORS",
+ "value": null,
+ "description": "A variable to configure jetty option: acceptors for
Carte"
+ },
+ {
+ "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE",
+ "value": null,
+ "description": "A variable to configure jetty option: acceptQueueSize
for Carte"
+ },
+ {
+ "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME",
+ "value": null,
+ "description": "A variable to configure jetty option:
lowResourcesMaxIdleTime for Carte"
+ },
+ {
+ "name":
"HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL",
+ "value": "N",
+ "description": "Set this variable to Y for backward compatibility for
the Merge Rows (diff) transform. Setting this to Y will use the data from the
reference stream (instead of the comparison stream) in case the compared rows
are identical."
+ },
+ {
+ "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE",
+ "value": "false",
+ "description": "Set this variable to false to preserve enclosure symbol
after splitting the string in the Split fields transform. Changing it to true
will remove first and last enclosure symbol from the resulting string chunks."
+ },
+ {
+ "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES",
+ "value": "false",
+ "description": "Set this variable to TRUE to allow your pipeline to pass
'null' fields and/or empty types."
+ },
+ {
+ "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT",
+ "value": "false",
+ "description": "Set this variable to false to preserve global log
variables defined in pipeline / workflow Properties -> Log panel. Changing it
to true will clear it when export pipeline / workflow."
+ },
+ {
+ "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT",
+ "value": "1024",
+ "description": "This project variable is used by the Text File Output
transform. It defines the max number of simultaneously open files within the
transform. The transform will close/reopen files as necessary to insure the max
is not exceeded"
+ },
+ {
+ "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE",
+ "value": "0",
+ "description": "This project variable is used by the Text File Output
transform. It defines the max number of milliseconds between flushes of files
opened by the transform."
+ },
+ {
+ "name": "HOP_USE_NATIVE_FILE_DIALOG",
+ "value": "N",
+ "description": "Set this value to Y if you want to use the system file
open/save dialog when browsing files"
+ },
+ {
+ "name": "HOP_AUTO_CREATE_CONFIG",
+ "value": "Y",
+ "description": "Set this value to N if you don't want to automatically
create a hop configuration file (hop-config.json) when it's missing"
+ }
+ ],
+ "LocaleDefault": "en_BE",
+ "guiProperties": {
+ "FontFixedSize": "13",
+ "MaxUndo": "100",
+ "DarkMode": "Y",
+ "FontNoteSize": "13",
+ "ShowOSLook": "Y",
+ "FontFixedStyle": "0",
+ "FontNoteName": ".AppleSystemUIFont",
+ "FontFixedName": "Monospaced",
+ "FontGraphStyle": "0",
+ "FontDefaultSize": "13",
+ "GraphColorR": "255",
+ "FontGraphSize": "13",
+ "IconSize": "32",
+ "BackgroundColorB": "255",
+ "FontNoteStyle": "0",
+ "FontGraphName": ".AppleSystemUIFont",
+ "FontDefaultName": ".AppleSystemUIFont",
+ "GraphColorG": "255",
+ "UseGlobalFileBookmarks": "Y",
+ "FontDefaultStyle": "0",
+ "GraphColorB": "255",
+ "BackgroundColorR": "255",
+ "BackgroundColorG": "255",
+ "WorkflowDialogStyle": "RESIZE,MAX,MIN",
+ "LineWidth": "1",
+ "ContextDialogShowCategories": "Y"
+ },
+ "projectsConfig": {
+ "enabled": true,
+ "projectMandatory": true,
+ "environmentMandatory": true,
+ "defaultProject": "default",
+ "defaultEnvironment": null,
+ "standardParentProject": "default",
+ "standardProjectsFolder": null,
+ "projectConfigurations": [
+ {
+ "projectName": "default",
+ "projectHome": "${HOP_CONFIG_FOLDER}",
+ "configFilename": "project-config.json"
+ }
+ ],
+ "lifecycleEnvironments": [
+ {
+ "name": "dev",
+ "purpose": "Testing",
+ "projectName": "default",
+ "configurationFiles": [
+ "${PROJECT_HOME}/dev-env-config.json"
+ ]
+ }
+ ],
+ "projectLifecycles": []
+ }
+}
\ No newline at end of file
diff --git a/integration-tests/ftp/main-001-ftp-put.hwf
b/integration-tests/ftp/main-001-ftp-put.hwf
new file mode 100644
index 0000000000..36601d3a14
--- /dev/null
+++ b/integration-tests/ftp/main-001-ftp-put.hwf
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-001-ftp-put</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2025/11/17 12:35:16.622</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/11/17 12:35:16.622</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>50</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Put a file with FTP</name>
+ <description/>
+ <type>FTP_PUT</type>
+ <attributes/>
+ <servername>${FTP_SERVER}</servername>
+ <serverport>21</serverport>
+ <username>${FTP_USER}</username>
+ <password>${FTP_PASSWORD}</password>
+ <remoteDirectory>/admin</remoteDirectory>
+ <localDirectory>${PROJECT_HOME}/files</localDirectory>
+ <wildcard>.*.txt</wildcard>
+ <binary>N</binary>
+ <timeout>0</timeout>
+ <remove>N</remove>
+ <only_new>N</only_new>
+ <active>N</active>
+ <control_encoding>ISO-8859-1</control_encoding>
+ <proxy_host/>
+ <proxy_port/>
+ <proxy_username/>
+ <proxy_password>Encrypted </proxy_password>
+ <socksproxy_host/>
+ <socksproxy_port>1080</socksproxy_port>
+ <socksproxy_username/>
+ <socksproxy_password>Encrypted </socksproxy_password>
+ <parallel>N</parallel>
+ <xloc>240</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Put a file with FTP</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/ftp/metadata/pipeline-run-configuration/local.json
b/integration-tests/ftp/metadata/pipeline-run-configuration/local.json
new file mode 100644
index 0000000000..0c0d87c84d
--- /dev/null
+++ b/integration-tests/ftp/metadata/pipeline-run-configuration/local.json
@@ -0,0 +1,22 @@
+{
+ "engineRunConfiguration": {
+ "Local": {
+ "feedback_size": "50000",
+ "sample_size": "100",
+ "sample_type_in_gui": "Last",
+ "wait_time": "20",
+ "rowset_size": "10000",
+ "safe_mode": false,
+ "show_feedback": false,
+ "topo_sort": false,
+ "gather_metrics": false,
+ "transactional": false
+ }
+ },
+ "defaultSelection": true,
+ "configurationVariables": [],
+ "name": "local",
+ "description": "",
+ "dataProfile": "",
+ "executionInfoLocationName": ""
+}
\ No newline at end of file
diff --git
a/integration-tests/ftp/metadata/workflow-run-configuration/local.json
b/integration-tests/ftp/metadata/workflow-run-configuration/local.json
new file mode 100644
index 0000000000..1d0cf74bae
--- /dev/null
+++ b/integration-tests/ftp/metadata/workflow-run-configuration/local.json
@@ -0,0 +1,11 @@
+{
+ "engineRunConfiguration": {
+ "Local": {
+ "safe_mode": false,
+ "transactional": false
+ }
+ },
+ "defaultSelection": true,
+ "name": "local",
+ "description": "Runs your workflows locally with the standard local Hop
workflow engine"
+}
\ No newline at end of file
diff --git a/integration-tests/ftp/project-config.json
b/integration-tests/ftp/project-config.json
new file mode 100644
index 0000000000..6a91171e1c
--- /dev/null
+++ b/integration-tests/ftp/project-config.json
@@ -0,0 +1,13 @@
+{
+ "metadataBaseFolder" : "${PROJECT_HOME}/metadata",
+ "unitTestsBasePath" : "${PROJECT_HOME}",
+ "dataSetsCsvFolder" : "${PROJECT_HOME}/datasets",
+ "enforcingExecutionInHome" : true,
+ "config" : {
+ "variables" : [ {
+ "name" : "HOP_LICENSE_HEADER_FILE",
+ "value" : "${PROJECT_HOME}/../asf-header.txt",
+ "description" : "This will automatically serialize the ASF license
header into pipelines and workflows in the integration test projects"
+ } ]
+ }
+}
\ No newline at end of file
diff --git a/plugins/actions/ftp/pom.xml b/plugins/actions/ftp/pom.xml
index 948b5d4fc6..8989cd10bc 100644
--- a/plugins/actions/ftp/pom.xml
+++ b/plugins/actions/ftp/pom.xml
@@ -31,7 +31,7 @@
<name>Hop Plugins Actions Get/Put/Delete a file with FTP/SFTP</name>
<properties>
- <commons-net-version>3.9.0</commons-net-version>
+ <commons-net-version>3.12.0</commons-net-version>
</properties>
<dependencyManagement>
diff --git
a/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPut.java
b/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPut.java
index 11dd1269d0..0cb5a7a314 100644
---
a/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPut.java
+++
b/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPut.java
@@ -18,15 +18,24 @@
package org.apache.hop.workflow.actions.ftpput;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
-import java.util.ArrayList;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
-import java.util.regex.Matcher;
+import java.util.Map;
import java.util.regex.Pattern;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPReply;
import org.apache.hop.core.Const;
import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.Result;
@@ -40,8 +49,6 @@ import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.core.xml.XmlHandler;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.metadata.api.IHopMetadataProvider;
-import org.apache.hop.resource.ResourceEntry;
-import org.apache.hop.resource.ResourceEntry.ResourceType;
import org.apache.hop.resource.ResourceReference;
import org.apache.hop.workflow.WorkflowMeta;
import org.apache.hop.workflow.action.ActionBase;
@@ -49,10 +56,13 @@ import org.apache.hop.workflow.action.IAction;
import org.apache.hop.workflow.action.validator.ActionValidatorUtils;
import org.apache.hop.workflow.action.validator.AndValidator;
import org.apache.hop.workflow.actions.util.FtpClientUtil;
+import org.apache.hop.workflow.actions.util.FtpHelper;
import org.apache.hop.workflow.actions.util.IFtpConnection;
import org.w3c.dom.Node;
/** This defines an FTP put action. */
+@Getter
+@Setter
@Action(
id = "FTP_PUT",
name = "i18n::ActionFTPPut.Name",
@@ -61,13 +71,15 @@ import org.w3c.dom.Node;
categoryDescription =
"i18n:org.apache.hop.workflow:ActionCategory.Category.FileTransfer",
keywords = "i18n::ActionFtpPut.keyword",
documentationUrl = "/workflow/actions/ftpput.html")
+@EqualsAndHashCode(callSuper = false)
public class ActionFtpPut extends ActionBase implements Cloneable, IAction,
IFtpConnection {
private static final Class<?> PKG = ActionFtpPut.class;
private static final String CONST_SPACE_SHORT = " ";
private static final String CONST_PASSWORD = "password";
private static final String CONST_LOCAL_DIRECTORY = "localDirectory";
- public static final int FTP_DEFAULT_PORT = 21;
+ public static final String FTP_DEFAULT_PORT = "21";
+ public static final String FTP_DEFAULT_PROXY_PORT = "1080";
private String serverName;
private String serverPort;
@@ -79,17 +91,16 @@ public class ActionFtpPut extends ActionBase implements
Cloneable, IAction, IFtp
private boolean binaryMode;
private int timeout;
private boolean remove;
- private boolean onlyPuttingNewFiles; /* Don't overwrite files */
+ /* Don't overwrite files */
+ private boolean onlyPuttingNewFiles;
private boolean activeConnection;
- private String controlEncoding; /* how to convert list of filenames e.g. */
+ /* how to convert list of filenames e.g. */
+ private String controlEncoding;
private String proxyHost;
-
- private String proxyPort; /* string to allow variable substitution */
-
+ /* string to allow variable substitution */
+ private String proxyPort;
private String proxyUsername;
-
private String proxyPassword;
-
private String socksProxyHost;
private String socksProxyPort;
private String socksProxyUsername;
@@ -104,8 +115,8 @@ public class ActionFtpPut extends ActionBase implements
Cloneable, IAction, IFtp
public ActionFtpPut(String n) {
super(n, "");
serverName = null;
- serverPort = "21";
- socksProxyPort = "1080";
+ serverPort = FTP_DEFAULT_PORT;
+ socksProxyPort = FTP_DEFAULT_PROXY_PORT;
remoteDirectory = null;
localDirectory = null;
setControlEncoding(DEFAULT_CONTROL_ENCODING);
@@ -115,55 +126,35 @@ public class ActionFtpPut extends ActionBase implements
Cloneable, IAction, IFtp
this("");
}
- @Override
- public Object clone() {
- ActionFtpPut je = (ActionFtpPut) super.clone();
- return je;
- }
-
@Override
public String getXml() {
- StringBuilder xml = new StringBuilder(450); // 365 characters in spaces
and tag names alone
-
+ Map<String, String> tags = new LinkedHashMap<>();
+ tags.put("servername", serverName);
+ tags.put("serverport", serverPort);
+ tags.put("username", userName);
+ tags.put(CONST_PASSWORD,
Encr.encryptPasswordIfNotUsingVariables(getPassword()));
+ tags.put("remoteDirectory", remoteDirectory);
+ tags.put(CONST_LOCAL_DIRECTORY, localDirectory);
+ tags.put("wildcard", wildcard);
+ tags.put("binary", binaryMode ? "Y" : "N");
+ tags.put("timeout", String.valueOf(timeout));
+ tags.put("remove", remove ? "Y" : "N");
+ tags.put("only_new", onlyPuttingNewFiles ? "Y" : "N");
+ tags.put("active", activeConnection ? "Y" : "N");
+ tags.put("control_encoding", controlEncoding);
+ tags.put("proxy_host", proxyHost);
+ tags.put("proxy_port", proxyPort);
+ tags.put("proxy_username", proxyUsername);
+ tags.put("proxy_password",
Encr.encryptPasswordIfNotUsingVariables(proxyPassword));
+ tags.put("socksproxy_host", socksProxyHost);
+ tags.put("socksproxy_port", socksProxyPort);
+ tags.put("socksproxy_username", socksProxyUsername);
+ tags.put("socksproxy_password",
Encr.encryptPasswordIfNotUsingVariables(socksProxyPassword));
+
+ // 365 characters in spaces and tag names alone
+ StringBuilder xml = new StringBuilder(450);
xml.append(super.getXml());
-
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("servername",
serverName));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("serverport",
serverPort));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("username",
userName));
- xml.append(CONST_SPACE_SHORT)
- .append(
- XmlHandler.addTagValue(
- CONST_PASSWORD,
Encr.encryptPasswordIfNotUsingVariables(getPassword())));
- xml.append(CONST_SPACE_SHORT)
- .append(XmlHandler.addTagValue("remoteDirectory", remoteDirectory));
- xml.append(CONST_SPACE_SHORT)
- .append(XmlHandler.addTagValue(CONST_LOCAL_DIRECTORY, localDirectory));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("wildcard",
wildcard));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("binary",
binaryMode));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("timeout",
timeout));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("remove",
remove));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("only_new",
onlyPuttingNewFiles));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("active",
activeConnection));
- xml.append(CONST_SPACE_SHORT)
- .append(XmlHandler.addTagValue("control_encoding", controlEncoding));
-
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("proxy_host",
proxyHost));
- xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("proxy_port",
proxyPort));
-
xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("proxy_username",
proxyUsername));
- xml.append(CONST_SPACE_SHORT)
- .append(
- XmlHandler.addTagValue(
- "proxy_password",
Encr.encryptPasswordIfNotUsingVariables(proxyPassword)));
-
xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("socksproxy_host",
socksProxyHost));
-
xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("socksproxy_port",
socksProxyPort));
- xml.append(CONST_SPACE_SHORT)
- .append(XmlHandler.addTagValue("socksproxy_username",
socksProxyUsername));
- xml.append(CONST_SPACE_SHORT)
- .append(
- XmlHandler.addTagValue(
- "socksproxy_password",
- Encr.encryptPasswordIfNotUsingVariables(socksProxyPassword)));
-
+ tags.forEach((k, v) ->
xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue(k, v)));
return xml.toString();
}
@@ -172,528 +163,84 @@ public class ActionFtpPut extends ActionBase implements
Cloneable, IAction, IFtp
throws HopXmlException {
try {
super.loadXml(entrynode);
- serverName = XmlHandler.getTagValue(entrynode, "servername");
- serverPort = XmlHandler.getTagValue(entrynode, "serverport");
- userName = XmlHandler.getTagValue(entrynode, "username");
- password =
- Encr.decryptPasswordOptionallyEncrypted(
- XmlHandler.getTagValue(entrynode, CONST_PASSWORD));
- remoteDirectory = XmlHandler.getTagValue(entrynode, "remoteDirectory");
- localDirectory = XmlHandler.getTagValue(entrynode,
CONST_LOCAL_DIRECTORY);
- wildcard = XmlHandler.getTagValue(entrynode, "wildcard");
- binaryMode = "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode,
"binary"));
- timeout = Const.toInt(XmlHandler.getTagValue(entrynode, "timeout"),
10000);
- remove = "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode,
"remove"));
- onlyPuttingNewFiles =
"Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, "only_new"));
- activeConnection =
"Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, "active"));
- controlEncoding = XmlHandler.getTagValue(entrynode, "control_encoding");
-
- proxyHost = XmlHandler.getTagValue(entrynode, "proxy_host");
- proxyPort = XmlHandler.getTagValue(entrynode, "proxy_port");
- proxyUsername = XmlHandler.getTagValue(entrynode, "proxy_username");
- proxyPassword =
- Encr.decryptPasswordOptionallyEncrypted(
- XmlHandler.getTagValue(entrynode, "proxy_password"));
- socksProxyHost = XmlHandler.getTagValue(entrynode, "socksproxy_host");
- socksProxyPort = XmlHandler.getTagValue(entrynode, "socksproxy_port");
- socksProxyUsername = XmlHandler.getTagValue(entrynode,
"socksproxy_username");
- socksProxyPassword =
- Encr.decryptPasswordOptionallyEncrypted(
- XmlHandler.getTagValue(entrynode, "socksproxy_password"));
-
- if (controlEncoding == null) {
+
+ serverName = extractString(entrynode, "servername");
+ serverPort = extractString(entrynode, "serverport");
+ userName = extractString(entrynode, "username");
+ password = extractDecrypted(entrynode, CONST_PASSWORD);
+ remoteDirectory = extractString(entrynode, "remoteDirectory");
+ localDirectory = extractString(entrynode, CONST_LOCAL_DIRECTORY);
+ wildcard = extractString(entrynode, "wildcard");
+ binaryMode = extractBoolean(entrynode, "binary");
+ timeout = extractTimeout(entrynode);
+ remove = extractBoolean(entrynode, "remove");
+ onlyPuttingNewFiles = extractBoolean(entrynode, "only_new");
+ activeConnection = extractBoolean(entrynode, "active");
+ controlEncoding = extractString(entrynode, "control_encoding");
+ proxyHost = extractString(entrynode, "proxy_host");
+ proxyPort = extractString(entrynode, "proxy_port");
+ proxyUsername = extractString(entrynode, "proxy_username");
+ proxyPassword = extractDecrypted(entrynode, "proxy_password");
+ socksProxyHost = extractString(entrynode, "socksproxy_host");
+ socksProxyPort = extractString(entrynode, "socksproxy_port");
+ socksProxyUsername = extractString(entrynode, "socksproxy_username");
+ socksProxyPassword = extractDecrypted(entrynode, "socksproxy_password");
+
+ if (Utils.isEmpty(controlEncoding)) {
// if we couldn't retrieve an encoding, assume it's an old instance and
// put in the the encoding used before v 2.4.0
controlEncoding = LEGACY_CONTROL_ENCODING;
}
- } catch (HopXmlException xe) {
+ } catch (HopXmlException ex) {
throw new HopXmlException(
- BaseMessages.getString(PKG, "ActionFtpPut.Log.UnableToLoadFromXml"),
xe);
+ BaseMessages.getString(PKG, "ActionFtpPut.Log.UnableToLoadFromXml"),
ex);
}
}
- /**
- * @return Returns the binaryMode.
- */
- @Override
- public boolean isBinaryMode() {
- return binaryMode;
- }
-
- /**
- * @param binaryMode The binaryMode to set.
- */
- public void setBinaryMode(boolean binaryMode) {
- this.binaryMode = binaryMode;
- }
-
- /**
- * @param timeout The timeout to set.
- */
- public void setTimeout(int timeout) {
- this.timeout = timeout;
- }
-
- /**
- * @return Returns the timeout.
- */
- @Override
- public int getTimeout() {
- return timeout;
- }
-
- /**
- * @return Returns the onlyGettingNewFiles.
- */
- public boolean isOnlyPuttingNewFiles() {
- return onlyPuttingNewFiles;
- }
-
- /**
- * @param onlyPuttingNewFiles Only transfer new files to the remote host
- */
- public void setOnlyPuttingNewFiles(boolean onlyPuttingNewFiles) {
- this.onlyPuttingNewFiles = onlyPuttingNewFiles;
- }
-
- /**
- * Get the control encoding to be used for ftp'ing
- *
- * @return the used encoding
- */
- @Override
- public String getControlEncoding() {
- return controlEncoding;
- }
-
- /**
- * Set the encoding to be used for ftp'ing. This determines how names are
translated in dir e.g.
- * It does impact the contents of the files being ftp'ed.
- *
- * @param encoding The encoding to be used.
- */
- public void setControlEncoding(String encoding) {
- this.controlEncoding = encoding;
- }
-
- /**
- * @return Returns the remoteDirectory.
- */
- public String getRemoteDirectory() {
- return remoteDirectory;
- }
-
- /**
- * @param directory The remoteDirectory to set.
- */
- public void setRemoteDirectory(String directory) {
- this.remoteDirectory = directory;
- }
-
- /**
- * @return Returns the password.
- */
- @Override
- public String getPassword() {
- return password;
- }
-
- /**
- * @param password The password to set.
- */
- public void setPassword(String password) {
- this.password = password;
- }
-
- /**
- * @return Returns the serverName.
- */
- @Override
- public String getServerName() {
- return serverName;
- }
-
- /**
- * @param serverName The serverName to set.
- */
- public void setServerName(String serverName) {
- this.serverName = serverName;
- }
-
- /**
- * @return Returns the userName.
- */
- @Override
- public String getUserName() {
- return userName;
- }
-
- /**
- * @param userName The userName to set.
- */
- public void setUserName(String userName) {
- this.userName = userName;
- }
-
- /**
- * @return Returns the wildcard.
- */
- public String getWildcard() {
- return wildcard;
- }
-
- /**
- * @param wildcard The wildcard to set.
- */
- public void setWildcard(String wildcard) {
- this.wildcard = wildcard;
- }
-
- /**
- * @return Returns the localDirectory.
- */
- public String getLocalDirectory() {
- return localDirectory;
- }
-
- /**
- * @param directory The localDirectory to set.
- */
- public void setLocalDirectory(String directory) {
- this.localDirectory = directory;
- }
-
- /**
- * @param remove The remove to set.
- */
- public void setRemove(boolean remove) {
- this.remove = remove;
- }
-
- /**
- * @return Returns the remove.
- */
- public boolean getRemove() {
- return remove;
- }
-
- @Override
- public String getServerPort() {
- return serverPort;
- }
-
- public void setServerPort(String serverPort) {
- this.serverPort = serverPort;
- }
-
- /**
- * @return the activeConnection
- */
- @Override
- public boolean isActiveConnection() {
- return activeConnection;
- }
-
- /**
- * @param activeConnection set to true to get an active FTP connection
- */
- public void setActiveConnection(boolean activeConnection) {
- this.activeConnection = activeConnection;
- }
-
- /**
- * @return Returns the hostname of the ftp-proxy.
- */
- @Override
- public String getProxyHost() {
- return proxyHost;
- }
-
- /**
- * @param proxyHost The hostname of the proxy.
- */
- public void setProxyHost(String proxyHost) {
- this.proxyHost = proxyHost;
- }
-
- /**
- * @return Returns the password which is used to authenticate at the proxy.
- */
- @Override
- public String getProxyPassword() {
- return proxyPassword;
- }
-
- /**
- * @param proxyPassword The password which is used to authenticate at the
proxy.
- */
- public void setProxyPassword(String proxyPassword) {
- this.proxyPassword = proxyPassword;
- }
-
- /**
- * @return Returns the port of the ftp-proxy.
- */
- @Override
- public String getProxyPort() {
- return proxyPort;
- }
-
- /**
- * @param proxyPort The port of the ftp-proxy.
- */
- public void setProxyPort(String proxyPort) {
- this.proxyPort = proxyPort;
- }
-
- /**
- * @return Returns the username which is used to authenticate at the proxy.
- */
- @Override
- public String getProxyUsername() {
- return proxyUsername;
- }
-
- /**
- * @param socksProxyHost The socks proxy host to set
- */
- public void setSocksProxyHost(String socksProxyHost) {
- this.socksProxyHost = socksProxyHost;
- }
-
- /**
- * @param socksProxyPort The socks proxy port to set
- */
- public void setSocksProxyPort(String socksProxyPort) {
- this.socksProxyPort = socksProxyPort;
- }
-
- /**
- * @param socksProxyUsername The socks proxy username to set
- */
- public void setSocksProxyUsername(String socksProxyUsername) {
- this.socksProxyUsername = socksProxyUsername;
- }
-
- /**
- * @param socksProxyPassword The socks proxy password to set
- */
- public void setSocksProxyPassword(String socksProxyPassword) {
- this.socksProxyPassword = socksProxyPassword;
- }
-
- /**
- * @return The sox proxy host name
- */
- @Override
- public String getSocksProxyHost() {
- return this.socksProxyHost;
- }
-
- /**
- * @return The socks proxy port
- */
@Override
- public String getSocksProxyPort() {
- return this.socksProxyPort;
- }
-
- /**
- * @return The socks proxy username
- */
- @Override
- public String getSocksProxyUsername() {
- return this.socksProxyUsername;
- }
-
- /**
- * @return The socks proxy password
- */
- @Override
- public String getSocksProxyPassword() {
- return this.socksProxyPassword;
- }
-
- /**
- * @param proxyUsername The username which is used to authenticate at the
proxy.
- */
- public void setProxyUsername(String proxyUsername) {
- this.proxyUsername = proxyUsername;
+ @SuppressWarnings("java:S2975")
+ public Object clone() {
+ return super.clone();
}
@Override
- public Result execute(Result previousResult, int nr) {
- Result result = previousResult;
- result.setResult(false);
+ public Result execute(Result prevResult, int nr) throws HopException {
+ prevResult.setResult(false);
long filesPut = 0;
if (isDetailed()) {
logDetailed(BaseMessages.getString(PKG, "ActionFtpPut.Log.Starting"));
}
- FTPClient ftpclient = null;
+ FTPClient ftpClient = null;
try {
- // Create ftp client to host:port ...
- ftpclient = createAndSetUpFtpClient();
-
- // move to spool dir ...
- String realRemoteDirectory = resolve(remoteDirectory);
- if (!Utils.isEmpty(realRemoteDirectory)) {
- ftpclient.changeWorkingDirectory(realRemoteDirectory);
- if (isDetailed()) {
- logDetailed(
- BaseMessages.getString(
- PKG, "ActionFtpPut.Log.ChangedDirectory",
realRemoteDirectory));
- }
- }
-
- String realLocalDirectory = resolve(localDirectory);
- if (realLocalDirectory == null) {
- throw new HopException(BaseMessages.getString(PKG,
"ActionFtpPut.LocalDir.NotSpecified"));
- } else {
- // handle file:/// prefix
- if (realLocalDirectory.startsWith("file:")) {
- realLocalDirectory = new URI(realLocalDirectory).getPath();
- }
- }
-
- final List<String> files;
- File localFiles = new File(realLocalDirectory);
- File[] children = localFiles.listFiles();
- if (children == null) {
- files = Collections.emptyList();
- } else {
- files = new ArrayList<>(children.length);
- for (File child : children) {
- // Get filename of file or directory
- if (!child.isDirectory()) {
- files.add(child.getName());
- }
- }
- }
- if (isDetailed()) {
- logDetailed(
- BaseMessages.getString(
- PKG,
- "ActionFtpPut.Log.FoundFileLocalDirectory",
- "" + files.size(),
- realLocalDirectory));
- }
-
- String realWildcard = resolve(wildcard);
- Pattern pattern;
- if (!Utils.isEmpty(realWildcard)) {
- pattern = Pattern.compile(realWildcard);
- } else {
- pattern = null;
- }
+ ftpClient = prepareFtpClient();
+ changeRemoteDirectory(ftpClient);
+ String realLocalDirectory = resolveLocalDirectory();
+ List<String> files = listLocalFiles(realLocalDirectory);
+ Pattern pattern = createPattern(resolve(wildcard));
+ // for the files and upload file
for (String file : files) {
if (parentWorkflow.isStopped()) {
break;
}
- boolean toBeProcessed = true;
-
- // First see if the file matches the regular expression!
- if (pattern != null) {
- Matcher matcher = pattern.matcher(file);
- toBeProcessed = matcher.matches();
- }
-
- if (toBeProcessed) {
- // File exists?
- boolean fileExist = false;
- try {
- fileExist = FtpClientUtil.fileExists(ftpclient, file);
- } catch (Exception e) {
- logError("Error checking for file existence on FTP server for
file: " + file, e);
- // Assume file does not exist !!
- }
-
- if (isDebug()) {
- if (fileExist) {
- logDebug(BaseMessages.getString(PKG,
"ActionFtpPut.Log.FileExists", file));
- } else {
- logDebug(BaseMessages.getString(PKG,
"ActionFtpPut.Log.FileDoesNotExists", file));
- }
- }
-
- if (!fileExist || !onlyPuttingNewFiles) {
- if (isDebug()) {
- logDebug(
- BaseMessages.getString(
- PKG,
- "ActionFtpPut.Log.PuttingFileToRemoteDirectory",
- file,
- realRemoteDirectory));
- }
-
- String localFilename = realLocalDirectory + Const.FILE_SEPARATOR +
file;
- try (InputStream inputStream =
HopVfs.getInputStream(localFilename)) {
- if (fileExist) {
- boolean deleted = ftpclient.deleteFile(file);
- if (!deleted) {
- logError(
- "Deletion of (existing) file '"
- + file
- + "' on the FTP server was not successful with reply
string: "
- + ftpclient.getReplyString());
- }
- }
- if (binaryMode) {
- ftpclient.setFileType(FTP.BINARY_FILE_TYPE);
- }
- boolean success = ftpclient.storeFile(file, inputStream);
- if (success) {
- filesPut++;
- } else {
- logError(
- "Transfer of file '"
- + localFilename
- + "' to the FTP server was not successful with reply
string: "
- + ftpclient.getReplyString());
- }
- }
-
- // Delete the file if this is needed!
- if (remove) {
- new File(localFilename).delete();
- if (isDetailed()) {
- logDetailed(
- BaseMessages.getString(PKG,
"ActionFtpPut.Log.DeletedFile", localFilename));
- }
- }
- }
+ if (shouldProcessFile(file, pattern) && uploadFile(ftpClient,
realLocalDirectory, file)) {
+ filesPut++;
+ deleteLocalFileIfNeeded(realLocalDirectory + Const.FILE_SEPARATOR +
file);
}
}
-
- result.setResult(true);
- if (isBasic()) {
- logBasic(BaseMessages.getString(PKG, "ActionFtpPut.Log.WeHavePut", ""
+ filesPut));
- }
+ // upload success.
+ prevResult.setResult(true);
+ logBasic(BaseMessages.getString(PKG, "ActionFtpPut.Log.WeHavePut", "" +
filesPut));
} catch (Exception e) {
- result.setNrErrors(1);
- logError(BaseMessages.getString(PKG,
"ActionFtpPut.Log.ErrorPuttingFiles", e.getMessage()));
+ prevResult.setNrErrors(1);
logError(Const.getStackTracker(e));
} finally {
- if (ftpclient != null && ftpclient.isConnected()) {
- try {
- ftpclient.quit();
- } catch (Exception e) {
- logError(BaseMessages.getString(PKG,
"ActionFtpPut.Log.ErrorQuitingFTP", e.getMessage()));
- }
- }
- FtpClientUtil.clearSocksJvmSettings();
+ closeFtpClient(ftpClient);
}
-
- return result;
- }
-
- // package-local visibility for testing purposes
- FTPClient createAndSetUpFtpClient() throws HopException {
-
- return FtpClientUtil.connectAndLogin(getLogChannel(), this, this,
getName());
+ return prevResult;
}
@Override
@@ -705,12 +252,8 @@ public class ActionFtpPut extends ActionBase implements
Cloneable, IAction, IFtp
public List<ResourceReference> getResourceDependencies(
IVariables variables, WorkflowMeta workflowMeta) {
List<ResourceReference> references = super.getResourceDependencies(this,
workflowMeta);
- if (!Utils.isEmpty(serverName)) {
- String realServerName = resolve(serverName);
- ResourceReference reference = new ResourceReference(this);
- reference.getEntries().add(new ResourceEntry(realServerName,
ResourceType.SERVER));
- references.add(reference);
- }
+ // add resource entity
+ FtpHelper.addServerResourceReferenceIfPresent(references, serverName,
this, this);
return references;
}
@@ -753,4 +296,189 @@ public class ActionFtpPut extends ActionBase implements
Cloneable, IAction, IFtp
remarks,
AndValidator.putValidators(ActionValidatorUtils.integerValidator()));
}
+
+ /**
+ * Creates the ftp client for this action.
+ *
+ * @return an initialized and connected {@link FTPClient}
+ * @throws HopException Exception if connection or setup fails
+ */
+ private FTPClient prepareFtpClient() throws HopException {
+ FTPClient ftpClient = createAndSetUpFtpClient();
+ if (ftpClient == null || !ftpClient.isConnected()) {
+ throw new HopException("Failed to connect FTP server.");
+ }
+
+ int code = ftpClient.getReplyCode();
+ String msg = ftpClient.getReplyString();
+ if (!FTPReply.isPositiveCompletion(code)) {
+ throw new HopException(
+ "FTP server refused connection. reply code: " + code + ", result: "
+ msg);
+ }
+
+ if (isBasic()) {
+ logBasic("FTP connection success, reply code: {0}, result: {1}", code,
msg);
+ }
+ return ftpClient;
+ }
+
+ /**
+ * Changes the current working directory on the FTP server to the resolved
remote directory.
+ *
+ * @param ftpClient ftpClient the {@link FTPClient} instance
+ * @throws IOException Exception if changing directory fails
+ */
+ private void changeRemoteDirectory(FTPClient ftpClient) throws IOException {
+ String realRemoteDirectory = resolve(remoteDirectory);
+ if (Utils.isEmpty(realRemoteDirectory)) {
+ return;
+ }
+
+ ftpClient.changeWorkingDirectory(realRemoteDirectory);
+ if (isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(PKG, "ActionFtpPut.Log.ChangedDirectory",
realRemoteDirectory));
+ }
+ }
+
+ /**
+ * Resolves the local directory path, handling "file:" prefixes
+ *
+ * @return the resolved local directory path
+ * @throws HopException if the local directory is not specified
+ * @throws URISyntaxException if the local directory is not specified
+ */
+ private String resolveLocalDirectory() throws HopException,
URISyntaxException {
+ String realLocalDirectory = resolve(localDirectory);
+ if (realLocalDirectory == null) {
+ throw new HopException(BaseMessages.getString(PKG,
"ActionFtpPut.LocalDir.NotSpecified"));
+ }
+
+ if (realLocalDirectory.startsWith("file:")) {
+ realLocalDirectory = new URI(realLocalDirectory).getPath();
+ }
+ return realLocalDirectory;
+ }
+
+ /**
+ * Lists all non-directory files in the given local directory.
+ *
+ * @param localDir the directory path
+ * @return a list of file names (excluding subdirectories)
+ */
+ private List<String> listLocalFiles(String localDir) {
+ File[] children = new File(localDir).listFiles();
+ if (children == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.stream(children).filter(f ->
!f.isDirectory()).map(File::getName).toList();
+ }
+
+ /**
+ * Compiles a regex {@link Pattern} from the given wildcard string.
+ *
+ * @param wildcard the wildcard string (may be null or empty)
+ * @return a {@link Pattern} object, or null if wildcard is empty
+ */
+ private Pattern createPattern(String wildcard) {
+ return Utils.isEmpty(wildcard) ? null : Pattern.compile(wildcard);
+ }
+
+ /**
+ * Checks whether a file should be processed based on the optional regex
pattern.
+ *
+ * @param file the file name
+ * @param pattern the compiled regex pattern (may be null)
+ * @return true if the file should be processed
+ */
+ private boolean shouldProcessFile(String file, Pattern pattern) {
+ return pattern == null || pattern.matcher(file).matches();
+ }
+
+ /**
+ * Uploads a single file to the FTP server, handling existing file deletion
and binary mode.
+ *
+ * @param ftpClient the {@link FTPClient} instance
+ * @param localDir the local directory containing the file
+ * @param file the file name
+ * @return true if the file was uploaded successfully, false otherwise
+ */
+ private boolean uploadFile(FTPClient ftpClient, String localDir, String
file) {
+ String localFilename = localDir + Const.FILE_SEPARATOR + file;
+ try (InputStream inputStream = HopVfs.getInputStream(localFilename)) {
+ boolean fileExist = FtpClientUtil.fileExists(ftpClient, file);
+ if (fileExist && !onlyPuttingNewFiles) {
+ ftpClient.deleteFile(file);
+ }
+
+ if (binaryMode) {
+ ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+ }
+
+ boolean success = ftpClient.storeFile(file, inputStream);
+ if (!success) {
+ logError("Failed to upload file '" + localFilename + "' → " +
ftpClient.getReplyString());
+ }
+ return success;
+ } catch (Exception e) {
+ logError("Error uploading file: " + localFilename, e);
+ return false;
+ }
+ }
+
+ /**
+ * Deletes the local file if the "remove" option is enabled.
+ *
+ * @param localFilename the full path of the local file to delete
+ */
+ private void deleteLocalFileIfNeeded(String localFilename) throws
IOException {
+ if (remove) {
+ Files.deleteIfExists(Path.of(localFilename));
+ if (isDetailed()) {
+ logDetailed(BaseMessages.getString(PKG,
"ActionFtpPut.Log.DeletedFile", localFilename));
+ }
+ }
+ }
+
+ /**
+ * Safely closes the FTP client connection.
+ *
+ * @param ftpClient the {@link FTPClient} instance to close
+ */
+ private void closeFtpClient(FTPClient ftpClient) {
+ if (ftpClient != null && ftpClient.isConnected()) {
+ try {
+ ftpClient.quit();
+ } catch (Exception e) {
+ logError(BaseMessages.getString(PKG,
"ActionFtpPut.Log.ErrorQuitingFTP", e.getMessage()));
+ }
+ }
+
+ FtpClientUtil.clearSocksJvmSettings();
+ }
+
+ // package-local visibility for testing purposes
+ FTPClient createAndSetUpFtpClient() throws HopException {
+ return FtpClientUtil.connectAndLogin(getLogChannel(), this, this,
getName());
+ }
+
+ /** extract boolean */
+ private boolean extractBoolean(Node node, String tagName) {
+ return "Y".equalsIgnoreCase(extractString(node, tagName));
+ }
+
+ /** extract timeout */
+ private int extractTimeout(Node node) {
+ return Const.toInt(extractString(node, "timeout"), 10000);
+ }
+
+ /** After extracting the string, decrypt it */
+ private String extractDecrypted(Node node, String tagName) {
+ return Encr.decryptPasswordOptionallyEncrypted(extractString(node,
tagName));
+ }
+
+ /** extract string */
+ private String extractString(Node node, String tagName) {
+ return XmlHandler.getTagValue(node, tagName);
+ }
}
diff --git
a/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutDialog.java
b/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutDialog.java
index 75f4aa180f..6d07895487 100644
---
a/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutDialog.java
+++
b/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutDialog.java
@@ -24,6 +24,7 @@ import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.util.Utils;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.FormDataBuilder;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.BaseDialog;
import org.apache.hop.ui.core.dialog.MessageBox;
@@ -136,11 +137,8 @@ public class ActionFtpPutDialog extends ActionDialog {
wName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
PropsUi.setLook(wName);
wName.addModifyListener(lsMod);
- FormData fdName = new FormData();
- fdName.left = new FormAttachment(middle, 0);
- fdName.top = new FormAttachment(0, margin);
- fdName.right = new FormAttachment(100, 0);
- wName.setLayoutData(fdName);
+ wName.setLayoutData(
+ FormDataBuilder.builder().left(middle, 0).top(0, margin).right(100,
0).build());
// The buttons at the bottom
//
@@ -440,11 +438,12 @@ public class ActionFtpPutDialog extends ActionDialog {
BaseMessages.getString(PKG, "ActionFtpPut.ControlEncoding.Tooltip"));
wControlEncoding.setItems(encodings);
PropsUi.setLook(wControlEncoding);
- FormData fdControlEncoding = new FormData();
- fdControlEncoding.left = new FormAttachment(middle, 0);
- fdControlEncoding.top = new FormAttachment(wlControlEncoding, 0,
SWT.CENTER);
- fdControlEncoding.right = new FormAttachment(100, 0);
- wControlEncoding.setLayoutData(fdControlEncoding);
+ wControlEncoding.setLayoutData(
+ FormDataBuilder.builder()
+ .left(middle, 0)
+ .top(wlControlEncoding, 0, SWT.CENTER)
+ .right(100, 0)
+ .build());
FormData fdAdvancedSettings = new FormData();
fdAdvancedSettings.left = new FormAttachment(0, margin);
@@ -827,11 +826,11 @@ public class ActionFtpPutDialog extends ActionDialog {
closeFtpConnection();
}
- private void checkRemoteFolder(String remoteFoldername) {
- if (!Utils.isEmpty(remoteFoldername) && connectToFtp(true,
remoteFoldername)) {
+ private void checkRemoteFolder(String remoteFolderName) {
+ if (!Utils.isEmpty(remoteFolderName) && connectToFtp(true,
remoteFolderName)) {
MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
mb.setMessage(
- BaseMessages.getString(PKG, "ActionFtpPut.FolderExists.OK",
remoteFoldername) + Const.CR);
+ BaseMessages.getString(PKG, "ActionFtpPut.FolderExists.OK",
remoteFolderName) + Const.CR);
mb.setText(BaseMessages.getString(PKG,
"ActionFtpPut.FolderExists.Title.Ok"));
mb.open();
}
@@ -907,7 +906,7 @@ public class ActionFtpPutDialog extends ActionDialog {
wRemoteDirectory.setText(Const.NVL(action.getRemoteDirectory(), ""));
wLocalDirectory.setText(Const.NVL(action.getLocalDirectory(), ""));
wWildcard.setText(Const.NVL(action.getWildcard(), ""));
- wRemove.setSelection(action.getRemove());
+ wRemove.setSelection(action.isRemove());
wBinaryMode.setSelection(action.isBinaryMode());
wTimeout.setText("" + action.getTimeout());
wOnlyNew.setSelection(action.isOnlyPuttingNewFiles());
diff --git
a/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/util/FtpHelper.java
b/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/util/FtpHelper.java
new file mode 100644
index 0000000000..4d643b8cb1
--- /dev/null
+++
b/plugins/actions/ftp/src/main/java/org/apache/hop/workflow/actions/util/FtpHelper.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.workflow.actions.util;
+
+import java.util.List;
+import lombok.experimental.UtilityClass;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.resource.IResourceHolder;
+import org.apache.hop.resource.ResourceEntry;
+import org.apache.hop.resource.ResourceReference;
+import org.apache.hop.workflow.action.ActionBase;
+
+/** ftp helper */
+@UtilityClass
+public class FtpHelper {
+
+ /**
+ * Adds a server {@link ResourceReference} to the given list if the provided
server name is not
+ * empty.
+ *
+ * @param references references the list of {@link ResourceReference}
objects to add to
+ * @param serverName serverName the server name to resolve and reference
+ * @param action actionBase
+ */
+ public static void addServerResourceReferenceIfPresent(
+ List<ResourceReference> references,
+ String serverName,
+ ActionBase action,
+ IResourceHolder holder) {
+ if (Utils.isEmpty(serverName)) {
+ return;
+ }
+
+ String realServerName = action.resolve(serverName);
+ ResourceReference reference = new ResourceReference(holder);
+ reference
+ .getEntries()
+ .add(new ResourceEntry(realServerName,
ResourceEntry.ResourceType.SERVER));
+ references.add(reference);
+ }
+}
diff --git
a/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutTests.java
b/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutTests.java
new file mode 100644
index 0000000000..ea4fb5d267
--- /dev/null
+++
b/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/ActionFtpPutTests.java
@@ -0,0 +1,237 @@
+/*
+ * 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.ftpput;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopXmlException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.resource.ResourceReference;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.w3c.dom.Node;
+
+/** ActionFtpPut test */
+class ActionFtpPutTests {
+ private ActionFtpPut action;
+
+ @BeforeEach
+ void setUp() throws HopException {
+ action = new ActionFtpPut("Test Put a file with FTP");
+ action.setUserName("user");
+ action.setPassword("password");
+ action.setServerName("127.0.0.1");
+ action.setName("Test name");
+ action.setRemoteDirectory("/home/user");
+ action.setLocalDirectory("/tmp");
+
+ HopEnvironment.init();
+ }
+
+ @Test
+ void testEmptyActionFtpPut() {
+ assertEquals("", action.getDescription());
+
+ ActionFtpPut ftpPut = new ActionFtpPut();
+ assertTrue(ftpPut.getDescription().isBlank());
+ }
+
+ @Test
+ void testClone() {
+ Object cloned = action.clone();
+ assertNotSame(cloned, action);
+ }
+
+ @Test
+ void testIsEvaluation() {
+ assertTrue(action.isEvaluation());
+ }
+
+ @Test
+ void testGetResourceDependencies() {
+ IVariables variables = mock(IVariables.class);
+ WorkflowMeta meta = mock(WorkflowMeta.class);
+
+ // 127.0.0.1 server
+ List<ResourceReference> references =
action.getResourceDependencies(variables, meta);
+ assertNotNull(references);
+ assertEquals(1, references.size());
+
+ // null server
+ action.setServerName(null);
+ references = action.getResourceDependencies(variables, meta);
+ assertNotNull(references);
+ assertTrue(references.isEmpty());
+ }
+
+ @Test
+ void testCheck() {
+ List<ICheckResult> remarks = new ArrayList<>();
+ WorkflowMeta workflowMeta = mock(WorkflowMeta.class);
+ IVariables variables = mock(IVariables.class);
+ IHopMetadataProvider provider = mock(IHopMetadataProvider.class);
+
+ // server is null
+ action.setServerName(Const.EMPTY_STRING);
+ action.check(remarks, workflowMeta, variables, provider);
+
+ boolean hasError =
+ remarks.stream().anyMatch(r -> r.getType() ==
ICheckResult.TYPE_RESULT_ERROR);
+ assertTrue(hasError);
+ }
+
+ @Test
+ void testGetXmlAndLoadXml() throws HopXmlException {
+ String xml = action.getXml();
+ assertNotNull(xml);
+
+ xml = "<action>" + xml + "</action>";
+ Node node = XmlHandler.loadXmlString(xml, "action");
+ ActionFtpPut loadedAction = new ActionFtpPut();
+ loadedAction.loadXml(node, null, null);
+
+ assertAll(
+ () -> {
+ assertEquals(action.getServerName(), loadedAction.getServerName());
+ assertEquals(action.getServerPort(), loadedAction.getServerPort());
+ assertEquals(action.getUserName(), loadedAction.getUserName());
+ assertEquals(action.getPassword(), loadedAction.getPassword());
+ assertEquals(action.getLocalDirectory(),
loadedAction.getLocalDirectory());
+ assertEquals(action.getRemoteDirectory(),
loadedAction.getRemoteDirectory());
+ assertEquals(action.getWildcard(), loadedAction.getWildcard());
+ assertEquals(action.isBinaryMode(), loadedAction.isBinaryMode());
+ assertEquals(action.getTimeout(), loadedAction.getTimeout());
+ assertEquals(action.isRemove(), loadedAction.isRemove());
+ assertEquals(action.isOnlyPuttingNewFiles(),
loadedAction.isOnlyPuttingNewFiles());
+ assertEquals(action.isActiveConnection(),
loadedAction.isActiveConnection());
+ assertEquals(action.getControlEncoding(),
loadedAction.getControlEncoding());
+ assertEquals(action.getProxyHost(), loadedAction.getProxyHost());
+ assertEquals(action.getProxyPort(), loadedAction.getProxyPort());
+ assertEquals(action.getProxyUsername(),
loadedAction.getProxyUsername());
+ assertEquals(
+ action.getProxyPassword() == null ? "" : null,
loadedAction.getProxyPassword());
+ assertEquals(action.getSocksProxyHost(),
loadedAction.getSocksProxyHost());
+ assertEquals(action.getSocksProxyPort(),
loadedAction.getSocksProxyPort());
+ assertEquals(action.getSocksProxyUsername(),
loadedAction.getSocksProxyUsername());
+ assertEquals(
+ action.getSocksProxyPassword() == null ? "" : null,
+ loadedAction.getSocksProxyPassword());
+ });
+ }
+
+ @Test
+ void testExecuteSuccess() throws Exception {
+ Path tempFile = Files.createTempFile(Path.of(action.getLocalDirectory()),
"file_", ".txt");
+
+ try (MockedStatic<HopVfs> ignored = mockStatic(HopVfs.class)) {
+ action = spy(new ActionFtpPut("Test FTP Action"));
+ action.setServerName("127.0.0.1");
+ action.setUserName("user");
+ action.setPassword("pass");
+ action.setRemoteDirectory("/remote");
+
+ // /tmp/directory
+ Path tempDir = Files.createTempDirectory("ftpTest");
+ action.setLocalDirectory(tempDir.toString());
+
+ FTPClient mockFtp = mock(FTPClient.class);
+ when(mockFtp.isConnected()).thenReturn(true);
+ when(mockFtp.storeFile(anyString(),
any(InputStream.class))).thenReturn(true);
+ when(mockFtp.getReplyCode()).thenReturn(230);
+ when(mockFtp.getReplyString()).thenReturn("OK");
+
+ doReturn(mockFtp).when(action).createAndSetUpFtpClient();
+
+ when(HopVfs.getInputStream(anyString()))
+ .thenAnswer(invocation -> new ByteArrayInputStream("test
content".getBytes()));
+
+ Result result = new Result();
+ result = action.execute(result, 0);
+
+ assertTrue(result.isResult(), "execute success");
+ assertEquals(0, result.getNrErrors());
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void testExecuteFailure() throws Exception {
+ Path tempFile = Files.createTempFile(Path.of(action.getLocalDirectory()),
"file_", ".txt");
+
+ try (MockedStatic<HopVfs> ignored = mockStatic(HopVfs.class)) {
+ action = spy(new ActionFtpPut("Test FTP Action"));
+ action.setServerName("127.0.0.1");
+ action.setUserName("user");
+ action.setPassword("pass");
+ action.setRemoteDirectory("/remote");
+
+ // /tmp/directory
+ Path tempDir = Files.createTempDirectory("ftpTest");
+ action.setLocalDirectory(tempDir.toString());
+
+ FTPClient mockFtp = mock(FTPClient.class);
+ when(mockFtp.isConnected()).thenReturn(true);
+ when(mockFtp.storeFile(anyString(),
any(InputStream.class))).thenReturn(true);
+ when(mockFtp.getReplyCode()).thenReturn(530);
+ when(mockFtp.getReplyString()).thenReturn("530 Login incorrect");
+
+ doReturn(mockFtp).when(action).createAndSetUpFtpClient();
+
+ when(HopVfs.getInputStream(anyString()))
+ .thenAnswer(invocation -> new ByteArrayInputStream("test
content".getBytes()));
+
+ Result result = new Result();
+ result = action.execute(result, 0);
+
+ assertFalse(result.isResult(), "530 Login incorrect");
+ assertEquals(1, result.getNrErrors());
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+}
diff --git
a/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/WorkflowActionFtpPutLoadSaveTest.java
b/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/WorkflowActionFtpPutLoadSaveTest.java
index 1bb873100d..51aa07f9af 100644
---
a/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/WorkflowActionFtpPutLoadSaveTest.java
+++
b/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/ftpput/WorkflowActionFtpPutLoadSaveTest.java
@@ -71,7 +71,7 @@ class WorkflowActionFtpPutLoadSaveTest extends
WorkflowActionLoadSaveTestSupport
"wildcard", "getWildcard",
"binary", "isBinaryMode",
"timeout", "getTimeout",
- "remove", "getRemove",
+ "remove", "isRemove",
"only_new", "isOnlyPuttingNewFiles",
"active", "isActiveConnection",
"control_encoding", "getControlEncoding",
diff --git
a/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/util/FtpHelperTests.java
b/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/util/FtpHelperTests.java
new file mode 100644
index 0000000000..3a2f1e28e4
--- /dev/null
+++
b/plugins/actions/ftp/src/test/java/org/apache/hop/workflow/actions/util/FtpHelperTests.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+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.Mockito.never;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.resource.IResourceHolder;
+import org.apache.hop.resource.ResourceEntry;
+import org.apache.hop.resource.ResourceReference;
+import org.apache.hop.workflow.action.ActionBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/** FtpHelper test */
+@ExtendWith(MockitoExtension.class)
+class FtpHelperTests {
+ @Mock private ActionBase action;
+ @Mock private IResourceHolder holder;
+ private List<ResourceReference> references;
+
+ @BeforeEach
+ void setUp() {
+ references = new ArrayList<>();
+ }
+
+ @Test
+ void shouldAddServerResourceWhenServerNameIsValid() {
+ String serverName = "test_server";
+ String resolveServer = "resolve_server";
+
+ when(action.resolve(serverName)).thenReturn(resolveServer);
+ FtpHelper.addServerResourceReferenceIfPresent(references, serverName,
action, holder);
+ assertEquals(1, references.size());
+
+ ResourceReference reference = references.get(0);
+ assertEquals(holder, reference.getReferenceHolder());
+ assertEquals(1, reference.getEntries().size());
+
+ ResourceEntry entry = reference.getEntries().get(0);
+ assertEquals(resolveServer, entry.getResource());
+ assertEquals(ResourceEntry.ResourceType.SERVER, entry.getResourcetype());
+ }
+
+ @Test
+ void shouldNotAddReferenceWhenServerNameIsEmpty() {
+ FtpHelper.addServerResourceReferenceIfPresent(references, null, action,
holder);
+ FtpHelper.addServerResourceReferenceIfPresent(references,
Const.EMPTY_STRING, action, holder);
+
+ assertTrue(references.isEmpty());
+ verify(action, never()).resolve(anyString());
+ }
+
+ @Test
+ void shouldUseResolveMethodToExpandVariables() {
+ String serverName = "test_server";
+ String resolveServer = "resolve_server";
+
+ when(action.resolve(serverName)).thenReturn(resolveServer);
+ FtpHelper.addServerResourceReferenceIfPresent(references, serverName,
action, holder);
+
+ verify(action, only()).resolve(serverName);
+ assertEquals(resolveServer,
references.get(0).getEntries().get(0).getResource());
+ }
+}
diff --git
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
index 9b37505c7f..8a2cd68011 100644
---
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
+++
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMeta.java
@@ -107,50 +107,24 @@ public class AddSequenceMeta extends
BaseTransformMeta<AddSequence, AddSequenceD
/**
* @param maxValue The maxValue to set.
*/
- public void setMaxValue(long maxValue) {
+ public void setMaxValueByValue(long maxValue) {
this.maxValue = Long.toString(maxValue);
}
- /**
- * @param maxValue The maxValue to set.
- */
- public void setMaxValue(String maxValue) {
- this.maxValue = maxValue;
- }
-
/**
* @param startAt The starting point of the sequence to set.
*/
- public void setStartAt(long startAt) {
+ public void setStartAtByValue(long startAt) {
this.startAt = Long.toString(startAt);
}
/**
* @param incrementBy The incrementBy to set.
*/
- public void setIncrementBy(long incrementBy) {
+ public void setIncrementByValue(long incrementBy) {
this.incrementBy = Long.toString(incrementBy);
}
- /**
- * @param incrementBy The incrementBy to set.
- */
- public void setIncrementBy(String incrementBy) {
- this.incrementBy = incrementBy;
- }
-
- /**
- * @param startAt The starting point of the sequence to set.
- */
- public void setStartAt(String startAt) {
- this.startAt = startAt;
- }
-
- @Override
- public Object clone() {
- return super.clone();
- }
-
@Override
public void setDefault() {
valueName = "valuename";
diff --git
a/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
b/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
index a72a3f6ee4..2c557ea085 100644
---
a/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
+++
b/plugins/transforms/addsequence/src/test/java/org/apache/hop/pipeline/transforms/addsequence/AddSequenceMetaTest.java
@@ -123,13 +123,13 @@ class AddSequenceMetaTest {
assertEquals("1000", meta.getMaxValue());
// Test long setters
- meta.setStartAt(100L);
+ meta.setStartAtByValue(100L);
assertEquals("100", meta.getStartAt());
- meta.setIncrementBy(10L);
+ meta.setIncrementByValue(10L);
assertEquals("10", meta.getIncrementBy());
- meta.setMaxValue(10000L);
+ meta.setMaxValueByValue(10000L);
assertEquals("10000", meta.getMaxValue());
}
diff --git
a/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/AddSnowflakeIdData.java
b/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/AddSnowflakeIdData.java
index 2b41b19bd9..d87b37f4e5 100644
---
a/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/AddSnowflakeIdData.java
+++
b/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/AddSnowflakeIdData.java
@@ -22,6 +22,7 @@ import org.apache.hop.pipeline.transform.BaseTransformData;
import org.apache.hop.pipeline.transform.ITransformData;
/** AddSnowflakeId data */
+@SuppressWarnings("java:S1104")
public class AddSnowflakeIdData extends BaseTransformData implements
ITransformData {
public IRowMeta outputRowMeta;
}
diff --git
a/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/SnowflakeSafeIdGenerator.java
b/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/SnowflakeSafeIdGenerator.java
index 41634e22de..74f6a6c294 100644
---
a/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/SnowflakeSafeIdGenerator.java
+++
b/plugins/transforms/addsnowflakeid/src/main/java/org/apache/hop/pipeline/transforms/addsnowflakeid/SnowflakeSafeIdGenerator.java
@@ -70,7 +70,9 @@ public class SnowflakeSafeIdGenerator {
*
* @return SnowflakeSafeIdGenerator
*/
+ @SuppressWarnings("java:S2245")
public static SnowflakeSafeIdGenerator createDefault() {
+ // ThreadLocalRandom is safe for non-crypto purposes
long dataCenterId = ThreadLocalRandom.current().nextInt(0, 32);
long machineId = ThreadLocalRandom.current().nextInt(0, 32);
return new SnowflakeSafeIdGenerator(dataCenterId, machineId, 10);