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 1b555e7ae5 Fix#8985 Delete folders workflow check results (#8984)
1b555e7ae5 is described below
commit 1b555e7ae5f4fa70ade7c6849cbf9af91670be20
Author: Lance <[email protected]>
AuthorDate: Tue Oct 20 20:40:47 2026 +0800
Fix#8985 Delete folders workflow check results (#8984)
* Fix Delete folders workflow check results
Signed-off-by: leehaut <[email protected]>
* Fix Delete folders workflow check results
Signed-off-by: leehaut <[email protected]>
* Fix Delete folders workflow check results
Signed-off-by: leehaut <[email protected]>
* Align Delete folders checks with runtime deletion
Signed-off-by: leehaut <[email protected]>
---------
Signed-off-by: leehaut <[email protected]>
---
.../actions/deletefolders/ActionDeleteFolders.java | 119 ++++++++--
.../messages/messages_en_US.properties | 4 +
.../messages/messages_zh_CN.properties | 4 +
.../ActionDeleteFoldersCheckTest.java | 260 +++++++++++++++++++++
4 files changed, 365 insertions(+), 22 deletions(-)
diff --git
a/plugins/actions/deletefolders/src/main/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFolders.java
b/plugins/actions/deletefolders/src/main/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFolders.java
index 4f23b7682a..91545f1ffd 100644
---
a/plugins/actions/deletefolders/src/main/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFolders.java
+++
b/plugins/actions/deletefolders/src/main/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFolders.java
@@ -20,6 +20,7 @@ package org.apache.hop.workflow.actions.deletefolders;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSelectInfo;
import org.apache.commons.vfs2.FileSelector;
@@ -43,6 +44,11 @@ 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;
+import org.apache.hop.workflow.action.validator.AbstractFileValidator;
+import org.apache.hop.workflow.action.validator.ActionValidatorUtils;
+import org.apache.hop.workflow.action.validator.AndValidator;
+import org.apache.hop.workflow.action.validator.FileExistsValidator;
+import org.apache.hop.workflow.action.validator.ValidatorContext;
/** This defines a 'delete folders' action. */
@Action(
@@ -155,8 +161,9 @@ public class ActionDeleteFolders extends ActionBase {
return result;
}
RowMetaAndData resultRow = rows.get(iteration);
+ // Result rows are data. Leave ${...} unresolved, as the other file
actions do.
String argsPrevious = resultRow.getString(0, null);
- if (!Utils.isEmpty(argsPrevious)) {
+ if (!StringUtils.isBlank(argsPrevious)) {
if (deleteFolder(argsPrevious)) {
updateSuccess();
} else {
@@ -179,7 +186,7 @@ public class ActionDeleteFolders extends ActionBase {
return result;
}
String realFileName = resolve(item.getFileName());
- if (!Utils.isEmpty(realFileName)) {
+ if (!StringUtils.isBlank(realFileName)) {
if (deleteFolder(realFileName)) {
updateSuccess();
} else {
@@ -292,32 +299,99 @@ public class ActionDeleteFolders extends ActionBase {
WorkflowMeta workflowMeta,
IVariables variables,
IHopMetadataProvider metadataProvider) {
- // boolean res =
- // ActionValidatorUtils.andValidator()
- // .validate(
- // this,
- // "fileItems",
- // remarks,
- //
AndValidator.putValidators(ActionValidatorUtils.notNullValidator()));
- //
- // if (!res) {
- // return;
- // }
+ // Folder names come from the previous action result at runtime.
+ if (argFromPrevious) {
+ return;
+ }
if (Utils.isEmpty(fileItems)) {
- String message = BaseMessages.getString(PKG,
"CloneRowMeta.CheckResult.NrClonesdMissing");
remarks.add(
- new CheckResult(ICheckResult.TYPE_RESULT_WARNING, "Any folders to
deletes", this));
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_WARNING,
+ BaseMessages.getString(PKG,
"ActionDeleteFolders.CheckResult.NoFolders"),
+ this));
+ return;
}
- /* TODO: If we enable action check
+ IVariables space = variables != null ? variables : getVariables();
ValidatorContext ctx = new ValidatorContext();
- AbstractFileValidator.putVariableSpace(ctx, getVariables());
+ AbstractFileValidator.putVariableSpace(ctx, space);
+ // A missing folder is a warning. Runtime treats it as already deleted,
and hop lint fails
+ // on ERROR by default.
+ FileExistsValidator.putFailIfDoesNotExist(ctx, true);
+ ActionValidatorUtils.putLevelOnFail(
+ ctx.getMap(), FileExistsValidator.INSTANCE.getName(),
ICheckResult.TYPE_RESULT_WARNING);
AndValidator.putValidators(
- ctx, ActionValidatorUtils.notNullValidator(),
ActionValidatorUtils.fileExistsValidator());
- for (FileItem item : fileItems) {
- ActionValidatorUtils.andValidator().validate(this, "fileName", remarks,
ctx);
- }*/
+ ctx, ActionValidatorUtils.notBlankValidator(),
ActionValidatorUtils.fileExistsValidator());
+
+ for (int i = 0; i < fileItems.size(); i++) {
+ String propertyName = "fileItems[" + i + "].fileName";
+ String folderName =
Const.NVL(space.resolve(fileItems.get(i).getFileName()), "");
+ if (skipsExistenceProbe(folderName)) {
+ ActionValidatorUtils.notBlankValidator().validate(this, propertyName,
remarks, ctx);
+ continue;
+ }
+ int sizeBefore = remarks.size();
+ if (!ActionValidatorUtils.andValidator().validate(this, propertyName,
remarks, ctx)) {
+ continue;
+ }
+ ICheckResult passed = remarks.size() > sizeBefore ? remarks.removeLast()
: null;
+ ICheckResult folderType = folderTypeRemark(space, folderName);
+ if (folderType != null) {
+ remarks.add(folderType);
+ } else if (passed != null) {
+ remarks.add(passed);
+ }
+ }
+ }
+
+ /**
+ * Background lint calls this method while the workflow is edited. Opening
sftp, s3, and similar
+ * URLs would block that pass on the network, so only local paths are probed.
+ */
+ private static boolean skipsExistenceProbe(String folderName) {
+ int separator = folderName.indexOf("://");
+ if (separator <= 0) {
+ return false;
+ }
+ return !"file".equalsIgnoreCase(folderName.substring(0, separator));
+ }
+
+ /** One remark for this row. {@code null} means the path is a folder and the
OK remark stands. */
+ private ICheckResult folderTypeRemark(IVariables space, String folderName) {
+ FileObject folder = null;
+ try {
+ folder = HopVfs.getFileObject(folderName, space);
+ if (!folder.exists()) {
+ return new CheckResult(
+ ICheckResult.TYPE_RESULT_WARNING,
+ BaseMessages.getString(
+ PKG, "ActionDeleteFolders.CheckResult.FolderDoesNotExist",
folderName),
+ this);
+ }
+ if (folder.getType() != FileType.FOLDER) {
+ return new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"ActionDeleteFolders.CheckResult.NotAFolder", folderName),
+ this);
+ }
+ return null;
+ } catch (Exception e) {
+ String detail = e.getMessage() == null ? e.getClass().getSimpleName() :
e.getMessage();
+ return new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG, "ActionDeleteFolders.CheckResult.CouldNotValidate",
folderName, detail),
+ this);
+ } finally {
+ if (folder != null) {
+ try {
+ folder.close();
+ } catch (Exception ignored) {
+ // close() must not add a second remark for the same row.
+ }
+ }
+ }
}
@Override
@@ -326,8 +400,9 @@ public class ActionDeleteFolders extends ActionBase {
List<ResourceReference> references =
super.getResourceDependencies(variables, workflowMeta);
ResourceReference reference = null;
+ IVariables space = variables != null ? variables : getVariables();
for (FileItem item : fileItems) {
- String filename = resolve(item.getFileName());
+ String filename = Const.NVL(space.resolve(item.getFileName()), "");
if (reference == null) {
reference = new ResourceReference(this);
references.add(reference);
diff --git
a/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_en_US.properties
b/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_en_US.properties
index 76776e1c08..4e55e7dd9c 100644
---
a/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_en_US.properties
+++
b/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_en_US.properties
@@ -15,6 +15,10 @@
# limitations under the License.
#
+ActionDeleteFolders.CheckResult.CouldNotValidate=Could not validate folder
[{0}], exception\: {1}
+ActionDeleteFolders.CheckResult.FolderDoesNotExist=Folder [{0}] does not exist.
+ActionDeleteFolders.CheckResult.NoFolders=No folders to delete
+ActionDeleteFolders.CheckResult.NotAFolder=[{0}] is not a folder.
ActionDeleteFolders.CouldNotDelete=Could not delete folder [{0}], exception\:
{1}
ActionDeleteFolders.Description=Delete specified folders.\nAttention : if a
the folder contains files, Apache Hop will delete them all!
ActionDeleteFolders.Error.EmptyLine=Empty line..ignoring
diff --git
a/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_zh_CN.properties
b/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_zh_CN.properties
index 2be2f3d757..41dcfbf3ac 100644
---
a/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_zh_CN.properties
+++
b/plugins/actions/deletefolders/src/main/resources/org/apache/hop/workflow/actions/deletefolders/messages/messages_zh_CN.properties
@@ -17,6 +17,10 @@
#
#
+ActionDeleteFolders.CheckResult.CouldNotValidate=\u65E0\u6CD5\u6821\u9A8C\u6587\u4EF6\u5939
[{0}]\uFF0C\u5F02\u5E38\uFF1A{1}
+ActionDeleteFolders.CheckResult.FolderDoesNotExist=\u6587\u4EF6\u5939 [{0}]
\u4E0D\u5B58\u5728\u3002
+ActionDeleteFolders.CheckResult.NoFolders=\u6CA1\u6709\u8981\u5220\u9664\u7684\u6587\u4EF6\u5939
+ActionDeleteFolders.CheckResult.NotAFolder=[{0}]
\u4E0D\u662F\u6587\u4EF6\u5939\u3002
ActionDeleteFolders.CouldNotDelete=\u65E0\u6CD5\u5220\u9664\u76EE\u5F55 [{0},
\u5F02\u5E38\: {1}
ActionDeleteFolders.Description=\u5220\u9664\u6307\u5B9A\u76EE\u5F55.\n\u63D0\u9192\:
Apache Hop
\u5C06\u5220\u9664\u8BE5\u76EE\u5F55\u53CA\u5176\u4E0B\u9762\u6240\u6709\u6587\u4EF6
ActionDeleteFolders.Error.EmptyLine=\u5FFD\u7565\u7A7A\u884C
diff --git
a/plugins/actions/deletefolders/src/test/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFoldersCheckTest.java
b/plugins/actions/deletefolders/src/test/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFoldersCheckTest.java
new file mode 100644
index 0000000000..65229464e4
--- /dev/null
+++
b/plugins/actions/deletefolders/src/test/java/org/apache/hop/workflow/actions/deletefolders/ActionDeleteFoldersCheckTest.java
@@ -0,0 +1,260 @@
+/*
+ * 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.deletefolders;
+
+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 java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.RowMetaAndData;
+import org.apache.hop.core.logging.HopLogStore;
+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.i18n.BaseMessages;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.resource.ResourceEntry;
+import org.apache.hop.resource.ResourceReference;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.validator.ValidatorMessages;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
+import org.apache.hop.workflow.engines.local.LocalWorkflowEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class ActionDeleteFoldersCheckTest {
+
+ @TempDir Path tempDir;
+
+ @BeforeAll
+ static void setUpClass() throws Exception {
+ HopClientEnvironment.init();
+ HopLogStore.init();
+ }
+
+ @Test
+ void emptyFolderListWarns() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+
+ List<ICheckResult> remarks = check(action, new Variables());
+
+ assertEquals(1, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_WARNING,
remarks.getFirst().getType());
+ assertEquals(
+ message("ActionDeleteFolders.CheckResult.NoFolders"),
remarks.getFirst().getText());
+ }
+
+ @Test
+ void previousResultsSkipTheFolderTable() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setArgFromPrevious(true);
+
+ assertTrue(check(action, new Variables()).isEmpty());
+ }
+
+ @Test
+ void missingFolderIsAWarning() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new
FileItem(tempDir.resolve("missing").toString())));
+
+ List<ICheckResult> remarks = check(action, new Variables());
+
+ assertEquals(1, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_WARNING,
remarks.getFirst().getType());
+ assertEquals(
+ ValidatorMessages.getString("messages.failed.fileExists",
"fileItems[0].fileName"),
+ remarks.getFirst().getText());
+ }
+
+ @Test
+ void existingFolderIsOk() throws Exception {
+ Path folder = tempDir.resolve("to-delete");
+ Files.createDirectories(folder);
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new FileItem(folder.toString())));
+
+ List<ICheckResult> remarks = check(action, new Variables());
+
+ assertEquals(1, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.getFirst().getType());
+ assertEquals(
+ passedMessage("fileItems[0].fileName", folder.toString()),
remarks.getFirst().getText());
+ }
+
+ @Test
+ void fileIsNotAFolder() throws Exception {
+ Path file = tempDir.resolve("a.txt");
+ Files.writeString(file, "x", StandardCharsets.UTF_8);
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new FileItem(file.toString())));
+
+ List<ICheckResult> remarks = check(action, new Variables());
+
+ assertEquals(1, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_ERROR, remarks.getFirst().getType());
+ assertEquals(
+ message("ActionDeleteFolders.CheckResult.NotAFolder", file.toString()),
+ remarks.getFirst().getText());
+ }
+
+ @Test
+ void blankFolderNameIsReported() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new FileItem(" ")));
+
+ List<ICheckResult> remarks = check(action, new Variables());
+
+ assertEquals(1, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_ERROR, remarks.getFirst().getType());
+ assertEquals(
+ ValidatorMessages.getString("messages.failed.notBlank",
"fileItems[0].fileName"),
+ remarks.getFirst().getText());
+ }
+
+ @Test
+ void variablePathResolvesBeforeTheCheck() throws Exception {
+ Path folder = tempDir.resolve("from-variable");
+ Files.createDirectories(folder);
+ Variables variables = new Variables();
+ variables.setVariable("FOLDER", folder.toString());
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new FileItem("${FOLDER}")));
+
+ List<ICheckResult> remarks = check(action, variables);
+
+ assertEquals(1, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.getFirst().getType());
+ assertEquals(passedMessage("fileItems[0].fileName", "${FOLDER}"),
remarks.getFirst().getText());
+ }
+
+ @Test
+ void remoteFolderIsNotProbed() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new FileItem("sftp://example.com/data")));
+
+ assertTrue(check(action, new Variables()).isEmpty());
+ }
+
+ @Test
+ void unknownSchemeIsNotProbed() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setFileItems(List.of(new FileItem("nosuchscheme://x")));
+
+ assertTrue(check(action, new Variables()).isEmpty());
+ }
+
+ @Test
+ void previousResultNameIsNotResolved() throws Exception {
+ Path kept = tempDir.resolve("kept");
+ Files.createDirectories(kept);
+ Files.writeString(kept.resolve("marker.txt"), "x", StandardCharsets.UTF_8);
+ ActionDeleteFolders action = attachedAction();
+ action.setArgFromPrevious(true);
+ action.setVariable("FOLDER", kept.toString());
+
+ Result result = action.execute(resultWithFolder("${FOLDER}"), 0);
+
+ assertEquals(0, result.getNrErrors());
+ assertTrue(Files.exists(kept.resolve("marker.txt")));
+ }
+
+ @Test
+ void previousResultDeletesTheLiteralFolder() throws Exception {
+ Path folder = tempDir.resolve("from-row");
+ Files.createDirectories(folder);
+ ActionDeleteFolders action = attachedAction();
+ action.setArgFromPrevious(true);
+
+ Result result = action.execute(resultWithFolder(folder.toString()), 0);
+
+ assertEquals(0, result.getNrErrors());
+ assertEquals(1, result.getNrLinesDeleted());
+ assertFalse(Files.exists(folder));
+ }
+
+ @Test
+ void blankNamesAreIgnoredAtRuntime() throws Exception {
+ ActionDeleteFolders action = attachedAction();
+ action.setFileItems(List.of(new FileItem(" ")));
+
+ Result result = action.execute(new Result(), 0);
+
+ assertEquals(0, result.getNrErrors());
+ assertEquals(0, result.getNrLinesDeleted());
+ assertTrue(result.getResult());
+ }
+
+ @Test
+ void resourceDependenciesUseTheGivenVariables() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ action.setVariable("FOLDER", "from-action");
+ action.setFileItems(List.of(new FileItem("${FOLDER}/data")));
+ Variables caller = new Variables();
+ caller.setVariable("FOLDER", "from-caller");
+
+ List<ResourceReference> references =
action.getResourceDependencies(caller, new WorkflowMeta());
+
+ assertEquals(1, references.size());
+ ResourceEntry entry = references.getFirst().getEntries().getFirst();
+ assertEquals(ResourceEntry.ResourceType.FILE, entry.getResourcetype());
+ assertEquals("from-caller/data", entry.getResource());
+ }
+
+ private static String passedMessage(String propertyName, String value) {
+ String shown = value;
+ if (value.length() > 20) {
+ shown = value.substring(0, 20) + "...";
+ }
+ return ValidatorMessages.getString("messages.passed", propertyName, shown);
+ }
+
+ private static ActionDeleteFolders attachedAction() {
+ ActionDeleteFolders action = new ActionDeleteFolders("delete folders");
+ IWorkflowEngine<WorkflowMeta> workflow = new LocalWorkflowEngine(new
WorkflowMeta());
+ workflow.setStopped(false);
+ action.setParentWorkflow(workflow);
+ return action;
+ }
+
+ private static Result resultWithFolder(String folderName) {
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("folder"));
+ Result result = new Result();
+ result.setRows(List.of(new RowMetaAndData(rowMeta, folderName)));
+ return result;
+ }
+
+ private static String message(String key, String... arguments) {
+ return BaseMessages.getString(ActionDeleteFolders.class, key, (Object[])
arguments);
+ }
+
+ private static List<ICheckResult> check(ActionDeleteFolders action,
Variables variables) {
+ List<ICheckResult> remarks = new ArrayList<>();
+ action.check(remarks, new WorkflowMeta(), variables, new
MemoryMetadataProvider());
+ return remarks;
+ }
+}