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 aa80d70746 issue #3153: add wildcards, exclude and subfolders to 
Checks if files exist (#7810)
aa80d70746 is described below

commit aa80d70746c15840c6649673794b1a73f5aee3cd
Author: Matt Casters <[email protected]>
AuthorDate: Thu Aug 6 20:39:41 2026 +0200

    issue #3153: add wildcards, exclude and subfolders to Checks if files exist 
(#7810)
    
    Allow the Checks if files exist action to specify per-row include/exclude
    RegExp wildcards and include-subfolders, aligned with other file actions
    and transforms, while keeping single-file File exists unchanged.
---
 .../ROOT/pages/workflow/actions/filesexist.adoc    |   9 +-
 .../actions/filesexist/ActionFilesExist.java       | 110 ++++++++++++++++++++-
 .../actions/filesexist/ActionFilesExistDialog.java |  82 +++++++++++----
 .../hop/workflow/actions/filesexist/FileItem.java  |  46 ++++++++-
 .../filesexist/messages/messages_en_US.properties  |  10 +-
 .../filesexist/WorkflowActionFilesExistTest.java   |  80 ++++++++++++++-
 .../src/test/resources/files-exist-action.xml      |   7 +-
 7 files changed, 309 insertions(+), 35 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/filesexist.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/filesexist.adoc
index f51f2a04ba..875d54be0f 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/filesexist.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/filesexist.adoc
@@ -26,11 +26,18 @@ under the License.
 
 The `Checks if files exist` action verifies that the specified files exist. 
The action returns `true` (success) if _all_ the listed files exist, `false` 
(fail) if _any_ of those don't.
 
+You can point to individual files, or to folders and use regular-expression 
wildcards (including an exclude wildcard) to verify that matching files are 
present. Optionally include subfolders per file/folder entry when evaluating 
wildcards.
+
 == Options
 
 [options="header", width="90%", cols="1,3"]
 |===
 |Option|Description
 |Action name|The name of the workflow action. This name must be unique in a 
single workflow.
-|Files/Folders|Specify the files and folders full paths to verify, one per 
line. You can use variables like `{openvar}PROJECT_HOME{closevar}` in the path, 
and they will be resolved.
+|Files/Folders|A table of files and folders to verify:
+
+* *File/Folder*: the full path of a file or folder. You can use variables like 
`{openvar}PROJECT_HOME{closevar}` in the path.
+* *Wildcard (RegExp)*: optional regular expression. When the path is a folder, 
at least one file matching this pattern must exist. When empty and no exclude 
wildcard is set, the action only checks that the path itself exists (file or 
folder).
+* *Exclude wildcard (RegExp)*: optional regular expression. Files matching 
this pattern are ignored when evaluating a folder.
+* *Include subfolders*: when set to `Y`, also look for matching files in 
subfolders of the specified folder.
 |===
diff --git 
a/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExist.java
 
b/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExist.java
index 86f06377b0..b92ebaa060 100644
--- 
a/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExist.java
+++ 
b/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExist.java
@@ -19,10 +19,15 @@ package org.apache.hop.workflow.actions.filesexist;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Pattern;
 import lombok.Getter;
 import lombok.Setter;
 import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSelectInfo;
+import org.apache.commons.vfs2.FileSelector;
+import org.apache.commons.vfs2.FileType;
 import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.Result;
 import org.apache.hop.core.annotations.Action;
@@ -81,13 +86,55 @@ public class ActionFilesExist extends ActionBase implements 
Cloneable, IAction {
 
     if (fileItems != null) {
       for (FileItem item : fileItems) {
-        if (parentWorkflow.isStopped()) break;
+        if (parentWorkflow.isStopped()) {
+          break;
+        }
 
         String path = resolve(item.getFileName());
+        String fileMask = resolve(item.getFileMask());
+        String excludeFileMask = resolve(item.getExcludeFileMask());
+        boolean includeSubfolders = item.isIncludeSubfolders();
+
         try (FileObject file = HopVfs.getFileObject(path, getVariables())) {
+          if (!file.exists()) {
+            missingFiles++;
+            if (isDetailed()) {
+              logDetailed(
+                  BaseMessages.getString(PKG, 
"ActionFilesExist.File_Does_Not_Exist", path));
+            }
+            continue;
+          }
 
-          if (file.exists()
-              && file.isReadable()) { // TODO: is it needed to check file for 
readability?
+          if (file.getType() == FileType.FOLDER
+              && (!Utils.isEmpty(fileMask) || 
!Utils.isEmpty(excludeFileMask))) {
+            // Folder with a wildcard: matching files must exist
+            FileObject[] matches =
+                file.findFiles(
+                    new TextFileSelector(
+                        file.toString(), fileMask, excludeFileMask, 
includeSubfolders));
+            if (matches == null || matches.length == 0) {
+              missingFiles++;
+              if (isDetailed()) {
+                logDetailed(
+                    BaseMessages.getString(
+                        PKG,
+                        "ActionFilesExist.NoMatchingFiles",
+                        path,
+                        Const.NVL(fileMask, ""),
+                        Const.NVL(excludeFileMask, "")));
+              }
+            } else {
+              if (isDetailed()) {
+                logDetailed(
+                    BaseMessages.getString(
+                        PKG,
+                        "ActionFilesExist.MatchingFilesFound",
+                        String.valueOf(matches.length),
+                        path));
+              }
+            }
+          } else if (file.isReadable()) {
+            // File or folder without wildcards: path must exist (and be 
readable)
             if (isDetailed()) {
               logDetailed(BaseMessages.getString(PKG, 
"ActionFilesExist.File_Exists", path));
             }
@@ -98,7 +145,6 @@ public class ActionFilesExist extends ActionBase implements 
Cloneable, IAction {
                   BaseMessages.getString(PKG, 
"ActionFilesExist.File_Does_Not_Exist", path));
             }
           }
-
         } catch (Exception e) {
           nrErrors++;
           missingFiles++;
@@ -118,6 +164,62 @@ public class ActionFilesExist extends ActionBase 
implements Cloneable, IAction {
     return result;
   }
 
+  private class TextFileSelector implements FileSelector {
+    private final String sourceFolder;
+    private final Pattern includePattern;
+    private final Pattern excludePattern;
+    private final boolean includeSubfolders;
+
+    public TextFileSelector(
+        String sourceFolder,
+        String fileWildcard,
+        String excludeWildcard,
+        boolean includeSubfolders) {
+      this.sourceFolder = sourceFolder;
+      this.includePattern = Utils.isEmpty(fileWildcard) ? null : 
Pattern.compile(fileWildcard);
+      this.excludePattern =
+          Utils.isEmpty(excludeWildcard) ? null : 
Pattern.compile(excludeWildcard);
+      this.includeSubfolders = includeSubfolders;
+    }
+
+    @Override
+    public boolean includeFile(FileSelectInfo info) {
+      try {
+        // Skip the base folder itself
+        if (info.getFile().toString().equals(sourceFolder)) {
+          return false;
+        }
+        if (info.getFile().getType() != FileType.FILE) {
+          return false;
+        }
+
+        // Only files in the base folder, or in sub-folders when enabled
+        boolean inBaseFolder = 
info.getFile().getParent().equals(info.getBaseFolder());
+        if (!inBaseFolder && !includeSubfolders) {
+          return false;
+        }
+
+        String shortFilename = info.getFile().getName().getBaseName();
+        boolean matches = includePattern == null || 
includePattern.matcher(shortFilename).matches();
+        boolean excluded =
+            excludePattern != null && 
excludePattern.matcher(shortFilename).matches();
+        return matches && !excluded;
+      } catch (Exception e) {
+        logError(
+            BaseMessages.getString(
+                PKG,
+                "ActionFilesExist.ERROR_0004_IO_Exception",
+                info.getFile() + " : " + e.getMessage()));
+        return false;
+      }
+    }
+
+    @Override
+    public boolean traverseDescendents(FileSelectInfo info) {
+      return info.getDepth() == 0 || includeSubfolders;
+    }
+  }
+
   @Override
   public boolean isEvaluation() {
     return true;
diff --git 
a/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExistDialog.java
 
b/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExistDialog.java
index 375c05121f..539ce79d5c 100644
--- 
a/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExistDialog.java
+++ 
b/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/ActionFilesExistDialog.java
@@ -19,8 +19,13 @@ package org.apache.hop.workflow.actions.filesexist;
 
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopFileException;
+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.core.vfs.HopVfs;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.dialog.BaseDialog;
@@ -45,6 +50,15 @@ import org.eclipse.swt.widgets.TableItem;
 public class ActionFilesExistDialog extends ActionDialog {
   private static final Class<?> PKG = ActionFilesExist.class;
 
+  private static final String[] FILETYPES =
+      new String[] {BaseMessages.getString(PKG, "System.FileType.AllFiles")};
+
+  private static final String[] YES_NO_COMBO =
+      new String[] {
+        BaseMessages.getString(PKG, "System.Combo.No"),
+        BaseMessages.getString(PKG, "System.Combo.Yes")
+      };
+
   private ActionFilesExist action;
 
   private boolean changed;
@@ -83,20 +97,28 @@ public class ActionFilesExistDialog extends ActionDialog {
               BaseMessages.getString(PKG, 
"ActionFilesExist.Fields.Argument.Label"),
               ColumnInfo.COLUMN_TYPE_TEXT_BUTTON,
               false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ActionFilesExist.Fields.Wildcard.Label"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ActionFilesExist.Fields.ExcludeWildcard.Label"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ActionFilesExist.Fields.IncludeSubfolders.Label"),
+              ColumnInfo.COLUMN_TYPE_CCOMBO,
+              YES_NO_COMBO),
         };
 
     columns[0].setUsingVariables(true);
     columns[0].setToolTip(BaseMessages.getString(PKG, 
"ActionFilesExist.Fields.Column"));
-    columns[0].setTextVarButtonSelectionListener(
-        new SelectionAdapter() {
-          @Override
-          public void widgetSelected(SelectionEvent arg0) {
-            String filename = BaseDialog.presentFileDialog(shell, null, null, 
true);
-            if (filename != null) {
-              
wFields.getActiveTableItem().setText(wFields.getActiveTableColumn(), filename);
-            }
-          }
-        });
+    columns[0].setTextVarButtonSelectionListener(getFileSelectionAdapter());
+    columns[1].setUsingVariables(true);
+    columns[1].setToolTip(BaseMessages.getString(PKG, 
"ActionFilesExist.Wildcard.Column"));
+    columns[2].setUsingVariables(true);
+    columns[2].setToolTip(BaseMessages.getString(PKG, 
"ActionFilesExist.ExcludeWildcard.Column"));
+    columns[3].setToolTip(BaseMessages.getString(PKG, 
"ActionFilesExist.IncludeSubfolders.Column"));
 
     wFields =
         new TableView(
@@ -114,7 +136,7 @@ public class ActionFilesExistDialog extends ActionDialog {
     fdFields.right = new FormAttachment(100, 0);
     fdFields.bottom = new FormAttachment(wCancel, -margin);
     wFields.setLayoutData(fdFields);
-    wFields.getTable().addListener(SWT.Resize, new ColumnsResizer(8, 92));
+    wFields.getTable().addListener(SWT.Resize, new ColumnsResizer(4, 40, 20, 
20, 16));
 
     getData();
     focusActionName();
@@ -124,6 +146,27 @@ public class ActionFilesExistDialog extends ActionDialog {
     return action;
   }
 
+  protected SelectionAdapter getFileSelectionAdapter() {
+    return new SelectionAdapter() {
+      @Override
+      public void widgetSelected(SelectionEvent event) {
+        try {
+          String path = 
wFields.getActiveTableItem().getText(wFields.getActiveTableColumn());
+          FileObject fileObject = HopVfs.getFileObject(path);
+
+          path =
+              BaseDialog.presentFileDialog(
+                  shell, null, variables, fileObject, new String[] {"*"}, 
FILETYPES, true);
+          if (path != null) {
+            
wFields.getActiveTableItem().setText(wFields.getActiveTableColumn(), path);
+          }
+        } catch (HopFileException e) {
+          LogChannel.UI.logError("Error selecting file or directory", e);
+        }
+      }
+    };
+  }
+
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
     if (action.getName() != null) {
@@ -133,12 +176,13 @@ public class ActionFilesExistDialog extends ActionDialog {
     int i = 0;
     for (FileItem item : action.getFileItems()) {
       TableItem ti = wFields.table.getItem(i++);
-      if (item.getFileName() != null) {
-        ti.setText(1, item.getFileName());
-      }
+      ti.setText(1, Const.NVL(item.getFileName(), ""));
+      ti.setText(2, Const.NVL(item.getFileMask(), ""));
+      ti.setText(3, Const.NVL(item.getExcludeFileMask(), ""));
+      ti.setText(4, item.isIncludeSubfolders() ? YES_NO_COMBO[1] : 
YES_NO_COMBO[0]);
     }
     wFields.setRowNums();
-    // wFields.optWidth(true);
+    wFields.optWidth(true);
   }
 
   @Override
@@ -165,9 +209,13 @@ public class ActionFilesExistDialog extends ActionDialog {
     int numberOfItems = wFields.nrNonEmpty();
     List<FileItem> items = new ArrayList<>();
     for (int i = 0; i < numberOfItems; i++) {
-      String path = wFields.getNonEmpty(i).getText(1);
+      TableItem tableItem = wFields.getNonEmpty(i);
+      String path = tableItem.getText(1);
+      String wildcard = tableItem.getText(2);
+      String excludeWildcard = tableItem.getText(3);
+      boolean includeSubfolders = 
YES_NO_COMBO[1].equalsIgnoreCase(tableItem.getText(4));
       if (!Utils.isEmpty(path)) {
-        items.add(new FileItem(path));
+        items.add(new FileItem(path, wildcard, excludeWildcard, 
includeSubfolders));
       }
     }
     action.setFileItems(items);
diff --git 
a/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/FileItem.java
 
b/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/FileItem.java
index 3d6d63b121..47ea6b1547 100644
--- 
a/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/FileItem.java
+++ 
b/plugins/actions/filesexist/src/main/java/org/apache/hop/workflow/actions/filesexist/FileItem.java
@@ -17,6 +17,7 @@
 
 package org.apache.hop.workflow.actions.filesexist;
 
+import java.util.Objects;
 import lombok.Getter;
 import lombok.Setter;
 import org.apache.hop.metadata.api.HopMetadataProperty;
@@ -25,26 +26,61 @@ import org.apache.hop.metadata.api.HopMetadataProperty;
 @Getter
 public class FileItem {
 
-  /** File name */
+  /** File or folder name */
   @HopMetadataProperty(key = "name")
   private String fileName;
 
+  /** Wildcard or file mask (regular expression) */
+  @HopMetadataProperty(key = "filemask")
+  private String fileMask;
+
+  /** Wildcard or file mask to exclude (regular expression) */
+  @HopMetadataProperty(key = "exclude_filemask")
+  private String excludeFileMask;
+
+  /** Whether to include subfolders when evaluating wildcards on a folder */
+  @HopMetadataProperty(key = "include_subfolders")
+  private boolean includeSubfolders;
+
   public FileItem() {}
 
   public FileItem(String fileName) {
+    this(fileName, null, null, false);
+  }
+
+  public FileItem(String fileName, String fileMask) {
+    this(fileName, fileMask, null, false);
+  }
+
+  public FileItem(String fileName, String fileMask, String excludeFileMask) {
+    this(fileName, fileMask, excludeFileMask, false);
+  }
+
+  public FileItem(
+      String fileName, String fileMask, String excludeFileMask, boolean 
includeSubfolders) {
     this.fileName = fileName;
+    this.fileMask = fileMask;
+    this.excludeFileMask = excludeFileMask;
+    this.includeSubfolders = includeSubfolders;
   }
 
   @Override
   public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
     FileItem item = (FileItem) o;
-    return fileName.equals(item.fileName);
+    return includeSubfolders == item.includeSubfolders
+        && Objects.equals(fileName, item.fileName)
+        && Objects.equals(fileMask, item.fileMask)
+        && Objects.equals(excludeFileMask, item.excludeFileMask);
   }
 
   @Override
   public int hashCode() {
-    return java.util.Objects.hash(fileName);
+    return Objects.hash(fileName, fileMask, excludeFileMask, 
includeSubfolders);
   }
 }
diff --git 
a/plugins/actions/filesexist/src/main/resources/org/apache/hop/workflow/actions/filesexist/messages/messages_en_US.properties
 
b/plugins/actions/filesexist/src/main/resources/org/apache/hop/workflow/actions/filesexist/messages/messages_en_US.properties
index 9e9fc4bb6d..ee7fe91a15 100644
--- 
a/plugins/actions/filesexist/src/main/resources/org/apache/hop/workflow/actions/filesexist/messages/messages_en_US.properties
+++ 
b/plugins/actions/filesexist/src/main/resources/org/apache/hop/workflow/actions/filesexist/messages/messages_en_US.properties
@@ -18,12 +18,20 @@
 ActionFilesExist.CheckResult.NothingToCheck=There's nothing to check
 ActionFilesExist.Description=Checks if files exists
 ActionFilesExist.ERROR_0004_IO_Exception=Unexpected error checking filename 
existance: {0}
+ActionFilesExist.ExcludeWildcard.Column=Enter a regular expression to exclude 
files (RegExp)
 ActionFilesExist.Fields.Argument.Label=File/Folder
-ActionFilesExist.Fields.Column=Specify here file to check
+ActionFilesExist.Fields.Column=Specify a file or folder to check
+ActionFilesExist.Fields.ExcludeWildcard.Label=Exclude wildcard (RegExp)
+ActionFilesExist.Fields.IncludeSubfolders.Label=Include subfolders
 ActionFilesExist.Fields.Label=Files/Folders
+ActionFilesExist.Fields.Wildcard.Label=Wildcard (RegExp)
 ActionFilesExist.File_Does_Not_Exist=File [{0}] does not exist\!
 ActionFilesExist.File_Exists=File [{0}] exists.
+ActionFilesExist.IncludeSubfolders.Column=Include files in subfolders when 
evaluating wildcards on a folder
 ActionFilesExist.Keyword=exist,check,verify,presence,filesystem
+ActionFilesExist.MatchingFilesFound=Found {0} matching file(s) in [{1}]
 ActionFilesExist.Name=Checks if files exist
 ActionFilesExist.Name.Default=Files exist
+ActionFilesExist.NoMatchingFiles=No files matching include [{1}] / exclude 
[{2}] were found in folder [{0}]
 ActionFilesExist.Title=Checks if files exist
+ActionFilesExist.Wildcard.Column=Specify a wildcard (RegExp) when the path is 
a folder
diff --git 
a/plugins/actions/filesexist/src/test/java/org/apache/hop/workflow/actions/filesexist/WorkflowActionFilesExistTest.java
 
b/plugins/actions/filesexist/src/test/java/org/apache/hop/workflow/actions/filesexist/WorkflowActionFilesExistTest.java
index 21f3cdf252..248e139cd3 100644
--- 
a/plugins/actions/filesexist/src/test/java/org/apache/hop/workflow/actions/filesexist/WorkflowActionFilesExistTest.java
+++ 
b/plugins/actions/filesexist/src/test/java/org/apache/hop/workflow/actions/filesexist/WorkflowActionFilesExistTest.java
@@ -23,8 +23,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 
 import java.util.List;
+import org.apache.commons.vfs2.FileObject;
 import org.apache.hop.core.Result;
 import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.utils.TestUtils;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.action.ActionMeta;
@@ -41,6 +43,7 @@ class WorkflowActionFilesExistTest {
 
   private String existingFile1;
   private String existingFile2;
+  private String folderPath;
 
   @BeforeAll
   static void setUpBeforeClass() {
@@ -48,7 +51,7 @@ class WorkflowActionFilesExistTest {
   }
 
   @BeforeEach
-  void setUp() {
+  void setUp() throws Exception {
     workflow = new LocalWorkflowEngine(new WorkflowMeta());
     action = new ActionFilesExist();
 
@@ -59,10 +62,18 @@ class WorkflowActionFilesExistTest {
 
     workflow.setStopped(false);
 
-    existingFile1 =
-        TestUtils.createRamFile(getClass().getSimpleName() + 
"/existingFile1.ext", action);
-    existingFile2 =
-        TestUtils.createRamFile(getClass().getSimpleName() + 
"/existingFile2.ext", action);
+    String base = getClass().getSimpleName();
+    existingFile1 = TestUtils.createRamFile(base + "/existingFile1.ext", 
action);
+    existingFile2 = TestUtils.createRamFile(base + "/existingFile2.ext", 
action);
+
+    folderPath = "ram://" + base + "/folder";
+    try (FileObject folder = HopVfs.getFileObject(folderPath, action)) {
+      folder.createFolder();
+    }
+    TestUtils.createRamFile(base + "/folder/match.txt", action);
+    TestUtils.createRamFile(base + "/folder/skip.tmp", action);
+    TestUtils.createRamFile(base + "/folder/other.log", action);
+    TestUtils.createRamFile(base + "/folder/sub/nested.txt", action);
   }
 
   @Test
@@ -72,7 +83,11 @@ class WorkflowActionFilesExistTest {
             "/files-exist-action.xml", ActionFilesExist.class);
 
     assertEquals("/folder", meta.getFileItems().get(0).getFileName());
+    assertEquals(".*\\.txt", meta.getFileItems().get(0).getFileMask());
+    assertEquals(".*\\.tmp", meta.getFileItems().get(0).getExcludeFileMask());
+    assertTrue(meta.getFileItems().get(0).isIncludeSubfolders());
     assertEquals("/archive.zip", meta.getFileItems().get(1).getFileName());
+    assertFalse(meta.getFileItems().get(1).isIncludeSubfolders());
     assertEquals(2, meta.getFileItems().size());
   }
 
@@ -120,4 +135,59 @@ class WorkflowActionFilesExistTest {
     Result res = action.execute(new Result(), 0);
     assertFalse(res.isResult());
   }
+
+  @Test
+  void testFolderExistsWithoutWildcard() {
+    action.setFileItems(List.of(new FileItem(folderPath)));
+
+    Result res = action.execute(new Result(), 0);
+    assertTrue(res.isResult(), "Folder path without wildcards should succeed 
when folder exists");
+  }
+
+  @Test
+  void testFolderWithMatchingWildcard() {
+    action.setFileItems(List.of(new FileItem(folderPath, ".*\\.txt")));
+
+    Result res = action.execute(new Result(), 0);
+    assertTrue(res.isResult(), "Matching files in folder should succeed");
+  }
+
+  @Test
+  void testFolderWithNonMatchingWildcard() {
+    action.setFileItems(List.of(new FileItem(folderPath, ".*\\.csv")));
+
+    Result res = action.execute(new Result(), 0);
+    assertFalse(res.isResult(), "No matching files should fail");
+  }
+
+  @Test
+  void testFolderWithExcludeWildcard() {
+    // Only skip.tmp would match .*, but exclude removes .tmp — match.txt and 
other.log remain
+    action.setFileItems(List.of(new FileItem(folderPath, ".*", ".*\\.tmp")));
+
+    Result res = action.execute(new Result(), 0);
+    assertTrue(res.isResult(), "Files remaining after exclude should succeed");
+  }
+
+  @Test
+  void testFolderWithExcludeRemovesAllMatches() {
+    // Include only .tmp, then exclude .tmp → nothing left
+    action.setFileItems(List.of(new FileItem(folderPath, ".*\\.tmp", 
".*\\.tmp")));
+
+    Result res = action.execute(new Result(), 0);
+    assertFalse(res.isResult(), "All matches excluded should fail");
+  }
+
+  @Test
+  void testIncludeSubfoldersPerRow() {
+    // nested.txt is in a subfolder; without includeSubfolders it is not found
+    action.setFileItems(List.of(new FileItem(folderPath, "nested\\.txt", null, 
false)));
+
+    Result withoutSub = action.execute(new Result(), 0);
+    assertFalse(withoutSub.isResult(), "Nested file should not match without 
subfolders");
+
+    action.setFileItems(List.of(new FileItem(folderPath, "nested\\.txt", null, 
true)));
+    Result withSub = action.execute(new Result(), 0);
+    assertTrue(withSub.isResult(), "Nested file should match with include 
subfolders");
+  }
 }
diff --git 
a/plugins/actions/filesexist/src/test/resources/files-exist-action.xml 
b/plugins/actions/filesexist/src/test/resources/files-exist-action.xml
index f75c523865..949c46ec79 100644
--- a/plugins/actions/filesexist/src/test/resources/files-exist-action.xml
+++ b/plugins/actions/filesexist/src/test/resources/files-exist-action.xml
@@ -21,17 +21,20 @@
     <description/>
     <type>FILES_EXIST</type>
     <attributes/>
-    <filename/>
     <fields>
         <field>
             <name>/folder</name>
+            <filemask>.*\.txt</filemask>
+            <exclude_filemask>.*\.tmp</exclude_filemask>
+            <include_subfolders>Y</include_subfolders>
         </field>
         <field>
             <name>/archive.zip</name>
+            <include_subfolders>N</include_subfolders>
         </field>
     </fields>
     <parallel>N</parallel>
     <xloc>160</xloc>
     <yloc>32</yloc>
     <attributes_hac/>
-</action>
\ No newline at end of file
+</action>

Reply via email to