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 d9bc33a2db Improve File Metadata error reporting for archive URIs 
(#8165)
d9bc33a2db is described below

commit d9bc33a2db3543674e1110fc340eb1cb560d2a72
Author: Sergio Ramazzina <[email protected]>
AuthorDate: Sat Aug 29 11:51:13 2026 +0200

    Improve File Metadata error reporting for archive URIs (#8165)
    
    - an archive URI without a file name after "!/" resolves to the archive
      root, which is a folder; detect it and fail with a message naming what
      is missing, instead of the VFS "because it is not a file" error
    - log the resolved file name when the file is not found, so the empty row
      the transform emits in that case is no longer silent
    - cover reading a CSV from inside a zip archive, which had no test
---
 .../transforms/filemetadata/FileMetadata.java      |  10 +-
 .../messages/messages_en_US.properties             |   2 +
 .../filemetadata/FileMetadataArchiveTest.java      | 156 +++++++++++++++++++++
 3 files changed, 167 insertions(+), 1 deletion(-)

diff --git 
a/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadata.java
 
b/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadata.java
index 6d32fd4264..15a38d58a9 100644
--- 
a/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadata.java
+++ 
b/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadata.java
@@ -30,6 +30,7 @@ import java.io.InputStreamReader;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import org.apache.commons.vfs2.FileSystemException;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.exception.HopFileException;
@@ -183,10 +184,17 @@ public class FileMetadata extends 
BaseTransform<FileMetadataMeta, FileMetadataDa
     // if the file does not exist, just send an empty row
     try {
       if (!HopVfs.fileExists(fileName, variables)) {
+        logBasic(BaseMessages.getString(PKG, "FileMetadata.Log.FileNotFound", 
fileName));
         putRow(data.outputRowMeta, outputRow);
         return;
       }
-    } catch (HopFileException e) {
+      // an archive URI without a file name after "!/" resolves to the archive 
root, which is a
+      // folder and cannot be read
+      if (HopVfs.getFileObject(fileName, variables).isFolder()) {
+        throw new HopTransformException(
+            BaseMessages.getString(PKG, "FileMetadata.Exception.NotAFile", 
fileName));
+      }
+    } catch (HopFileException | FileSystemException e) {
       throw new HopTransformException(e.getMessage(), e);
     }
 
diff --git 
a/plugins/transforms/filemetadata/src/main/resources/org/apache/hop/pipeline/transforms/filemetadata/messages/messages_en_US.properties
 
b/plugins/transforms/filemetadata/src/main/resources/org/apache/hop/pipeline/transforms/filemetadata/messages/messages_en_US.properties
index 18ac4a6eb7..5df13e7178 100644
--- 
a/plugins/transforms/filemetadata/src/main/resources/org/apache/hop/pipeline/transforms/filemetadata/messages/messages_en_US.properties
+++ 
b/plugins/transforms/filemetadata/src/main/resources/org/apache/hop/pipeline/transforms/filemetadata/messages/messages_en_US.properties
@@ -22,6 +22,8 @@
 FileMetadata.DelimitedLayout.Group.Label=Layout configuration
 FileMetadata.Exception.FileNameFieldNotFound=File name field [{0}] couldn''t 
be found in the input stream\!
 FileMetadata.Exception.FileNameNotSet=Filename not set\!
+FileMetadata.Exception.NotAFile=[{0}] is a folder, not a file. When reading 
from an archive, name the file inside it after "\!/", for example 
zip:file:///path/archive.zip\!/data.csv
+FileMetadata.Log.FileNotFound=File [{0}] not found, sending an empty row. When 
reading from an archive, the name after "\!/" is case sensitive and must 
include any folder path inside the archive.
 FileMetadata.FailedToGetFields.DialogMessage=Error
 FileMetadata.FailedToGetFields.DialogTitle=Error getting fields\!
 FileMetadata.FileInField.Label=Filename in a field?
diff --git 
a/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadataArchiveTest.java
 
b/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadataArchiveTest.java
new file mode 100644
index 0000000000..b673f06b58
--- /dev/null
+++ 
b/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadataArchiveTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.filemetadata;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.apache.hop.core.BlockingRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Covers reading a CSV from inside an archive through a layered VFS URI, and 
the two ways such a
+ * URI can be malformed.
+ */
+class FileMetadataArchiveTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private static final String ENTRY_NAME = "report.csv";
+  private static final String CSV_CONTENT = 
"colA;colB;colC\n1;2;3\n4;5;6\n7;8;9\n";
+
+  @TempDir private Path tempDir;
+
+  private TransformMockHelper<FileMetadataMeta, FileMetadataData> mockHelper;
+  private String zipUri;
+
+  @BeforeEach
+  void setUp() throws IOException {
+    mockHelper =
+        new TransformMockHelper<>("FileMetadata", FileMetadataMeta.class, 
FileMetadataData.class);
+    when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(mockHelper.iLogChannel);
+    when(mockHelper.pipeline.isRunning()).thenReturn(true);
+
+    Path zipFile = tempDir.resolve("report.zip");
+    try (ZipOutputStream zip = new 
ZipOutputStream(Files.newOutputStream(zipFile))) {
+      zip.putNextEntry(new ZipEntry(ENTRY_NAME));
+      zip.write(CSV_CONTENT.getBytes(StandardCharsets.UTF_8));
+      zip.closeEntry();
+    }
+    zipUri = "zip:" + zipFile.toUri();
+  }
+
+  @AfterEach
+  void tearDown() {
+    mockHelper.cleanUp();
+  }
+
+  @Test
+  void readsCsvFromInsideZipArchive() throws Exception {
+    BlockingRowSet output = new BlockingRowSet(10);
+    FileMetadata transform = createTransform(zipUri + "!/" + ENTRY_NAME, 
output);
+
+    assertFalse(transform.processRow(), "the transform generates its rows and 
is then done");
+
+    // one row per detected column
+    Object[] first = output.getRowWait(1, TimeUnit.SECONDS);
+    assertNotNull(first, "expected metadata rows for the CSV inside the 
archive");
+    assertEquals("US-ASCII", first[0].toString());
+    assertEquals(';', first[1]);
+    assertEquals("colA", first[7]);
+
+    assertEquals("colB", output.getRowWait(1, TimeUnit.SECONDS)[7]);
+    assertEquals("colC", output.getRowWait(1, TimeUnit.SECONDS)[7]);
+  }
+
+  @Test
+  void archiveWithoutEntryNameFailsWithAnActionableMessage() {
+    FileMetadata transform = createTransform(zipUri, new BlockingRowSet(10));
+
+    HopException exception = assertThrows(HopException.class, 
transform::processRow);
+
+    String expected =
+        BaseMessages.getString(FileMetadata.class, 
"FileMetadata.Exception.NotAFile", zipUri);
+    assertEquals(expected.trim(), exception.getMessage().trim());
+  }
+
+  @Test
+  void unknownEntryNameLogsInsteadOfFailingSilently() throws Exception {
+    OutputStream log = new ByteArrayOutputStream();
+    mockHelper.redirectLog(log, LogLevel.BASIC);
+
+    String missing = zipUri + "!/does-not-exist.csv";
+    BlockingRowSet output = new BlockingRowSet(10);
+    FileMetadata transform = createTransform(missing, output);
+
+    assertFalse(transform.processRow(), "a missing file must not abort the 
pipeline");
+
+    Object[] row = output.getRowWait(1, TimeUnit.SECONDS);
+    assertNotNull(row, "an empty row is still emitted");
+    assertNull(row[0], "the row carries no metadata");
+
+    assertTrue(
+        log.toString().contains(missing),
+        () -> "the log should name the file that was not found, but was: " + 
log);
+  }
+
+  private FileMetadata createTransform(String fileName, BlockingRowSet output) 
{
+    FileMetadataMeta meta = new FileMetadataMeta();
+    meta.setDefault();
+    meta.setFileName(fileName);
+
+    when(mockHelper.transformMeta.getTransform()).thenReturn(meta);
+    FileMetadata transform =
+        new FileMetadata(
+            mockHelper.transformMeta,
+            meta,
+            new FileMetadataData(),
+            0,
+            mockHelper.pipelineMeta,
+            mockHelper.pipeline);
+    transform.addRowSetToOutputRowSets(output);
+    return transform;
+  }
+}

Reply via email to