From 6fa2efa8576ef185e405ad87ace6fe2b30a87fb4 Mon Sep 17 00:00:00 2001 From: Hans Van Akelyen Date: Fri, 24 Jul 2026 15:12:52 +0200 Subject: [PATCH] unzip action wildcard support, fixes #5943 --- .../workflow/actions/unzip/ActionUnZip.java | 29 ++-- .../unzip/WorkflowActionUnZipTest.java | 159 ++++++++++++++++++ 2 files changed, 177 insertions(+), 11 deletions(-) diff --git a/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java b/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java index 778b7553b1c..dc3b2adaa97 100644 --- a/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java +++ b/plugins/actions/unzip/src/main/java/org/apache/hop/workflow/actions/unzip/ActionUnZip.java @@ -24,11 +24,11 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.Getter; import lombok.Setter; import org.apache.commons.vfs2.AllFileSelector; +import org.apache.commons.vfs2.FileName; import org.apache.commons.vfs2.FileObject; import org.apache.commons.vfs2.FileSelectInfo; import org.apache.commons.vfs2.FileSystemException; @@ -482,10 +482,6 @@ private boolean processOneFile( if (!children[i].getType().equals(FileType.FOLDER)) { boolean unzip = true; - // Match RegExp against the file name only (like other file actions). getPath() is the - // full path from the FS root, so patterns such as "daily_.*\\.zip" never matched. - String sourceMatchName = children[i].getName().getBaseName(); - Pattern patternSource = null; if (!Utils.isEmpty(realWildcardSource)) { @@ -494,8 +490,7 @@ private boolean processOneFile( // First see if the file matches the regular expression! if (patternSource != null) { - Matcher matcher = patternSource.matcher(sourceMatchName); - unzip = matcher.matches(); + unzip = matchesWildcard(patternSource, children[i]); } if (unzip) { if (!unzipFile( @@ -672,13 +667,11 @@ public boolean includeFile(FileSelectInfo info) { // First see if the file matches the regular expression! // if (pattern != null) { - Matcher matcher = pattern.matcher(item.getName().getURI()); - getIt = matcher.matches(); + getIt = matchesWildcard(pattern, item); } if (patternexclude != null) { - Matcher matcherexclude = patternexclude.matcher(item.getName().getURI()); - getItexclude = matcherexclude.matches(); + getItexclude = matchesWildcard(patternexclude, item); } boolean take = takeThisFile(item, newFileName); @@ -975,6 +968,20 @@ && getSuccessCondition().equals(SUCCESS_IF_AT_LEAST_X_FILES_UN_ZIPPED)) return retval; } + /** + * Matches a compiled wildcard against a file. To stay consistent with the shared file readers + * ({@link org.apache.hop.core.fileinput.FileInputList}, used by Text File Input, Get File Names, + * ...) the pattern is tested against the file's base name, while also keeping the historical + * behavior of matching the full path/URI so that path-style patterns (those that include a + * directory prefix, as reported in issue #5943) keep working. A file matches when either form + * matches. + */ + private static boolean matchesWildcard(Pattern pattern, FileObject file) { + FileName name = file.getName(); + return pattern.matcher(name.getBaseName()).matches() + || pattern.matcher(name.getURI()).matches(); + } + private boolean takeThisFile(FileObject sourceFile, String destinationFile) throws FileSystemException { boolean retval = false; diff --git a/plugins/actions/unzip/src/test/java/org/apache/hop/workflow/actions/unzip/WorkflowActionUnZipTest.java b/plugins/actions/unzip/src/test/java/org/apache/hop/workflow/actions/unzip/WorkflowActionUnZipTest.java index 1bb603d6c17..190b5490f19 100644 --- a/plugins/actions/unzip/src/test/java/org/apache/hop/workflow/actions/unzip/WorkflowActionUnZipTest.java +++ b/plugins/actions/unzip/src/test/java/org/apache/hop/workflow/actions/unzip/WorkflowActionUnZipTest.java @@ -17,17 +17,30 @@ package org.apache.hop.workflow.actions.unzip; 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.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.apache.commons.vfs2.FileContent; import org.apache.commons.vfs2.FileName; import org.apache.commons.vfs2.FileObject; import org.apache.commons.vfs2.FileType; import org.apache.hop.core.Result; import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.apache.hop.workflow.WorkflowMeta; +import org.apache.hop.workflow.action.ActionMeta; import org.apache.hop.workflow.action.loadsave.WorkflowActionLoadSaveTestSupport; +import org.apache.hop.workflow.engine.IWorkflowEngine; +import org.apache.hop.workflow.engines.local.LocalWorkflowEngine; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; @@ -36,6 +49,20 @@ class WorkflowActionUnZipTest extends WorkflowActionLoadSaveTestSupport getActionClass() { return ActionUnZip.class; @@ -89,4 +116,136 @@ void unzipPostProcessingTest() throws Exception { Mockito.verify(sourceForMove, Mockito.times(1)).moveTo(Mockito.any()); assertEquals(77L, moveResult.getBytesWrittenThisAction()); } + + /** + * Issue #5943: unzip a folder of archives selected with a base-name source wildcard. This + * is the convention shared with every other folder+wildcard reader in Hop (Text File Input, Get + * File Names, ... via {@link org.apache.hop.core.fileinput.FileInputList}), which matches the + * wildcard against the file's base name. Both {@code geoip_*.zip} archives should be extracted. + */ + @Test + void unzipFolderWithSourceWildcardMatchesBaseName() throws Exception { + File sourceDir = createGeoipArchiveFolder(); + File targetDir = new File(sourceDir, "extract"); + + Result result = runUnzipFolder(sourceDir, targetDir, SOURCE_WILDCARD_BASENAME, null); + + assertEquals(0, result.getNrErrors(), "The action should not report errors"); + assertEquals( + 2, + result.getNrLinesWritten(), + "Both geoip_*.zip archives should have been matched and unzipped"); + assertTrue( + new File(targetDir, "country.txt").exists(), + "country.txt from geoip_country.zip should have been extracted"); + assertTrue( + new File(targetDir, "city.txt").exists(), + "city.txt from geoip_city.zip should have been extracted"); + } + + /** + * Issue #5943 regression: the same scenario using the path-style source wildcard from the last + * comment of the issue ({@link #SOURCE_WILDCARD_PATH}, which contains a directory separator). + * This pattern only matched before 2.18; the action now also matches the full path/URI so it + * keeps working. Without the fix this extracts nothing ("Nr unzipped files : 0") without an + * error. + */ + @Test + void unzipFolderWithPathStyleSourceWildcard() throws Exception { + File sourceDir = createGeoipArchiveFolder(); + File targetDir = new File(sourceDir, "extract"); + + Result result = runUnzipFolder(sourceDir, targetDir, SOURCE_WILDCARD_PATH, null); + + assertEquals(0, result.getNrErrors(), "The action should not report errors"); + assertEquals( + 2, + result.getNrLinesWritten(), + "Both geoip_*.zip archives should have been matched by the path-style wildcard"); + assertTrue( + new File(targetDir, "country.txt").exists(), + "country.txt from geoip_country.zip should have been extracted"); + assertTrue( + new File(targetDir, "city.txt").exists(), + "city.txt from geoip_city.zip should have been extracted"); + } + + /** + * The wildcard selecting entries inside a zip ({@code wildcard}) is now matched against + * both the entry's base name and its full URI, consistent with the source wildcard. This verifies + * a plain base-name entry pattern extracts only the matching entry. + */ + @Test + void unzipEntryWildcardMatchesBaseName() throws Exception { + File sourceDir = Files.createTempDirectory("unzip5943-entry").toFile(); + sourceDir.deleteOnExit(); + createZipWithEntries( + new File(sourceDir, "geoip.zip"), + new String[] {"country.txt", "city.txt"}, + new String[] {"country-data", "city-data"}); + File targetDir = new File(sourceDir, "extract"); + + // Select the single zip with a base-name source wildcard, and only the "country.txt" entry + // inside it with a base-name entry wildcard. + Result result = runUnzipFolder(sourceDir, targetDir, "geoip\\.zip", "country\\.txt"); + + assertEquals(0, result.getNrErrors(), "The action should not report errors"); + assertTrue( + new File(targetDir, "country.txt").exists(), + "country.txt should have been extracted (matched the entry wildcard)"); + assertFalse( + new File(targetDir, "city.txt").exists(), + "city.txt should have been filtered out by the entry wildcard"); + } + + /** + * Builds a temp folder with two {@code geoip_*.zip} archives, each holding one distinct entry. + */ + private static File createGeoipArchiveFolder() throws java.io.IOException { + File sourceDir = Files.createTempDirectory("unzip5943-src").toFile(); + sourceDir.deleteOnExit(); + createZipWithEntries( + new File(sourceDir, "geoip_country.zip"), + new String[] {"country.txt"}, + new String[] {"country-data"}); + createZipWithEntries( + new File(sourceDir, "geoip_city.zip"), + new String[] {"city.txt"}, + new String[] {"city-data"}); + return sourceDir; + } + + /** Configures and runs the unzip action over a folder of archives, returning its result. */ + private static Result runUnzipFolder( + File sourceDir, File targetDir, String sourceWildcard, String entryWildcard) { + IWorkflowEngine workflow = new LocalWorkflowEngine(new WorkflowMeta()); + workflow.setStopped(false); + + ActionUnZip action = new ActionUnZip(); + workflow.getWorkflowMeta().addAction(new ActionMeta(action)); + action.setParentWorkflow(workflow); + action.setParentWorkflowMeta(workflow.getWorkflowMeta()); + + action.setZipFilename(sourceDir.getPath()); // Zip file name = a folder + action.setWildcardSource(sourceWildcard); // which archives in the folder + action.setWildcard(entryWildcard); // which entries inside each archive + action.setSourceDirectory(targetDir.getPath()); // Target directory (on screen) + action.setCreateFolder(true); + action.setIfFileExist(FileExistsEnum.SKIP); + + return action.execute(new Result(), 0); + } + + /** Creates a zip file containing the given text entries. */ + private static void createZipWithEntries(File zipFile, String[] entryNames, String[] contents) + throws java.io.IOException { + try (OutputStream fos = new FileOutputStream(zipFile); + ZipOutputStream zos = new ZipOutputStream(fos)) { + for (int i = 0; i < entryNames.length; i++) { + zos.putNextEntry(new ZipEntry(entryNames[i])); + zos.write(contents[i].getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + } + } }