This is an automated email from the ASF dual-hosted git repository.

pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new b0168a6eeda NIFI-15758 Added Fragment Attributes to FlowFile format in 
UnpackContent (#11526)
b0168a6eeda is described below

commit b0168a6eedac3cdf7310a87d1aac4e49635a0e82
Author: David Handermann <[email protected]>
AuthorDate: Tue Aug 11 07:19:15 2026 -0500

    NIFI-15758 Added Fragment Attributes to FlowFile format in UnpackContent 
(#11526)
---
 .../nifi/processors/standard/UnpackContent.java    | 116 ++++++++++++---------
 .../processors/standard/TestUnpackContent.java     |  66 ++++++++++++
 2 files changed, 133 insertions(+), 49 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/UnpackContent.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/UnpackContent.java
index 3100e6982fb..63bc8273d83 100644
--- 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/UnpackContent.java
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/UnpackContent.java
@@ -311,32 +311,13 @@ public class UnpackContent extends AbstractProcessor {
             }
         }
 
-        // set the Unpacker to use for this FlowFile.  FlowFileUnpackager 
objects maintain state and are not reusable.
-        final Unpacker unpacker;
-        final boolean addFragmentAttrs = switch (packagingFormat) {
-            case TAR_FORMAT -> {
-                unpacker = tarUnpacker;
-                yield true;
-            }
-            case ZIP_FORMAT -> {
-                unpacker = zipUnpacker;
-                yield true;
-            }
-            case FLOWFILE_STREAM_FORMAT_V2 -> {
-                unpacker = new FlowFileStreamUnpacker(new 
FlowFileUnpackagerV2());
-                yield false;
-            }
-            case FLOWFILE_STREAM_FORMAT_V3 -> {
-                unpacker = new FlowFileStreamUnpacker(new 
FlowFileUnpackagerV3());
-                yield false;
-            }
-            case FLOWFILE_TAR_FORMAT -> {
-                unpacker = new FlowFileStreamUnpacker(new 
FlowFileUnpackagerV1());
-                yield false;
-            }
-            default ->
-                // The format of the unpacker should be known before 
initialization
-                throw new ProcessException(packagingFormat + " is not a valid 
packaging format");
+        final Unpacker unpacker = switch (packagingFormat) {
+            case TAR_FORMAT -> tarUnpacker;
+            case ZIP_FORMAT -> zipUnpacker;
+            case FLOWFILE_STREAM_FORMAT_V2 -> new FlowFileStreamUnpacker(new 
FlowFileUnpackagerV2());
+            case FLOWFILE_STREAM_FORMAT_V3 -> new FlowFileStreamUnpacker(new 
FlowFileUnpackagerV3());
+            case FLOWFILE_TAR_FORMAT -> new FlowFileStreamUnpacker(new 
FlowFileUnpackagerV1());
+            default -> throw new ProcessException("Format [%s] not 
supported".formatted(packagingFormat));
         };
 
         final List<FlowFile> unpacked = new ArrayList<>();
@@ -348,9 +329,17 @@ public class UnpackContent extends AbstractProcessor {
                 return;
             }
 
-            if (addFragmentAttrs) {
-                finishFragmentAttributes(session, flowFile, unpacked);
+            // Determine whether Fragment Identifiers are needed based on 
Unpacker and existing attribute status after unpacking
+            final boolean fragmentIdentifiersRequired;
+            if (unpacker instanceof final FlowFileStreamUnpacker 
streamUnpacker) {
+                fragmentIdentifiersRequired = 
!streamUnpacker.isFragmentIdentifierRestored();
+            } else {
+                fragmentIdentifiersRequired = true;
             }
+            if (fragmentIdentifiersRequired) {
+                putFragmentAttributes(session, flowFile, unpacked);
+            }
+
             session.transfer(unpacked, REL_SUCCESS);
             final String fragmentId = !unpacked.isEmpty() ? 
unpacked.getFirst().getAttribute(FRAGMENT_ID) : null;
             flowFile = FragmentAttributes.copyAttributesToOriginal(session, 
flowFile, fragmentId, unpacked.size());
@@ -648,10 +637,16 @@ public class UnpackContent extends AbstractProcessor {
 
         private final FlowFileUnpackager unpackager;
 
+        private boolean fragmentIdentifierRestored;
+
         public FlowFileStreamUnpacker(final FlowFileUnpackager unpackager) {
             this.unpackager = unpackager;
         }
 
+        private boolean isFragmentIdentifierRestored() {
+            return fragmentIdentifierRestored;
+        }
+
         @Override
         public void unpack(final ProcessSession session, final FlowFile 
source, final List<FlowFile> unpacked) {
             session.read(source, inputStream -> {
@@ -677,6 +672,13 @@ public class UnpackContent extends AbstractProcessor {
                             // and later unpack it -- in this case, we have 
two FlowFiles with the same UUID.
                             attributes.remove(CoreAttributes.UUID.key());
 
+                            // Track whether the packaged FlowFile itself 
carried a fragment identifier. This is evaluated
+                            // against the restored attributes rather than the 
unpacked FlowFile, because a child created
+                            // from the source inherits the source's 
attributes and could otherwise report a false positive.
+                            if (attributes.containsKey(FRAGMENT_ID)) {
+                                fragmentIdentifierRestored = true;
+                            }
+
                             if 
(!attributes.containsKey(CoreAttributes.MIME_TYPE.key())) {
                                 attributes.put(CoreAttributes.MIME_TYPE.key(), 
OCTET_STREAM);
                             }
@@ -691,33 +693,49 @@ public class UnpackContent extends AbstractProcessor {
         }
     }
 
-    private void finishFragmentAttributes(final ProcessSession session, final 
FlowFile source, final List<FlowFile> unpacked) {
-        // first pass verifies all FlowFiles have the FRAGMENT_INDEX attribute 
and gets the total number of fragments
-        int fragmentCount = 0;
-        for (FlowFile ff : unpacked) {
-            String fragmentIndex = ff.getAttribute(FRAGMENT_INDEX);
-            if (fragmentIndex != null) {
-                fragmentCount++;
-            } else {
-                return;
+    private void putFragmentAttributes(
+            final ProcessSession session,
+            final FlowFile source,
+            final List<FlowFile> unpacked
+    ) {
+        final String fragmentId = UUID.randomUUID().toString();
+        final String segmentOriginalFilename = 
getSegmentOriginalFilename(source);
+        final String fragmentCount = String.valueOf(unpacked.size());
+
+        final List<FlowFile> updated = new ArrayList<>(unpacked.size());
+        int fragmentIndex = 0;
+        for (final FlowFile unpackedFlowFile : unpacked) {
+            fragmentIndex++;
+            final Map<String, String> fragmentAttributes = new HashMap<>();
+
+            final String unpackedFragmentId = 
unpackedFlowFile.getAttribute(FRAGMENT_ID);
+            if (unpackedFragmentId == null) {
+                // Set Fragment Identifier and Index when absent for FlowFile 
Formats
+                fragmentAttributes.put(FRAGMENT_ID, fragmentId);
+                fragmentAttributes.put(FRAGMENT_INDEX, 
String.valueOf(fragmentIndex));
             }
-        }
 
-        String originalFilename = 
source.getAttribute(CoreAttributes.FILENAME.key());
-        if (originalFilename.endsWith(".tar") || 
originalFilename.endsWith(".zip") || originalFilename.endsWith(".pkg")) {
-            originalFilename = originalFilename.substring(0, 
originalFilename.length() - 4);
+            fragmentAttributes.put(FRAGMENT_COUNT, fragmentCount);
+            fragmentAttributes.put(SEGMENT_ORIGINAL_FILENAME, 
segmentOriginalFilename);
+
+            updated.add(session.putAllAttributes(unpackedFlowFile, 
fragmentAttributes));
         }
 
-        // second pass adds fragment attributes
-        List<FlowFile> newList = new ArrayList<>(unpacked);
         unpacked.clear();
-        for (FlowFile ff : newList) {
-            FlowFile newFF = session.putAllAttributes(ff, Map.of(
-                    FRAGMENT_COUNT, String.valueOf(fragmentCount),
-                    SEGMENT_ORIGINAL_FILENAME, originalFilename
-            ));
-            unpacked.add(newFF);
+        unpacked.addAll(updated);
+    }
+
+    private String getSegmentOriginalFilename(final FlowFile source) {
+        final String filename = 
source.getAttribute(CoreAttributes.FILENAME.key());
+
+        final String originalFilename;
+        if (filename.endsWith(".tar") || filename.endsWith(".zip") || 
filename.endsWith(".pkg")) {
+            originalFilename = filename.substring(0, filename.length() - 4);
+        } else {
+            originalFilename = filename;
         }
+
+        return originalFilename;
     }
 
     protected enum PackageFormat implements DescribedValue {
diff --git 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestUnpackContent.java
 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestUnpackContent.java
index 630f19d157c..80727a826b9 100644
--- 
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestUnpackContent.java
+++ 
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestUnpackContent.java
@@ -45,12 +45,17 @@ import static 
org.apache.nifi.processors.standard.SplitContent.FRAGMENT_COUNT;
 import static org.apache.nifi.processors.standard.SplitContent.FRAGMENT_ID;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class TestUnpackContent {
 
     private static final String FIRST_FRAGMENT_INDEX = "1";
 
+    private static final String EXISTING_FRAGMENT_ID = "existing-fragment-id";
+
+    private static final String EXISTING_SEGMENT_FILENAME = "original-archive";
+
     private static final Path dataPath = 
Paths.get("src/test/resources/TestUnpackContent");
 
     private static final DateTimeFormatter TIMESTAMP_FORMATTER = 
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
@@ -451,6 +456,67 @@ public class TestUnpackContent {
         }
     }
 
+    @Test
+    public void testFlowFileStreamAssignsFragmentAttributesWhenAbsent() throws 
IOException {
+        runner.setProperty(UnpackContent.PACKAGING_FORMAT, 
UnpackContent.PackageFormat.FLOWFILE_STREAM_FORMAT_V3);
+        runner.enqueue(dataPath.resolve("data.flowfilev3"));
+        runner.run();
+
+        runner.assertTransferCount(UnpackContent.REL_SUCCESS, 2);
+        runner.assertTransferCount(UnpackContent.REL_FAILURE, 0);
+
+        final List<MockFlowFile> unpacked = 
runner.getFlowFilesForRelationship(UnpackContent.REL_SUCCESS);
+        final String fragmentId = 
unpacked.getFirst().getAttribute(UnpackContent.FRAGMENT_ID);
+        assertNotNull(fragmentId);
+        for (final MockFlowFile flowFile : unpacked) {
+            flowFile.assertAttributeEquals(UnpackContent.FRAGMENT_ID, 
fragmentId);
+            flowFile.assertAttributeEquals(UnpackContent.FRAGMENT_COUNT, "2");
+            
flowFile.assertAttributeEquals(UnpackContent.SEGMENT_ORIGINAL_FILENAME, 
"data.flowfilev3");
+        }
+
+        unpacked.get(0).assertAttributeEquals(UnpackContent.FRAGMENT_INDEX, 
FIRST_FRAGMENT_INDEX);
+        unpacked.get(1).assertAttributeEquals(UnpackContent.FRAGMENT_INDEX, 
"2");
+    }
+
+    @Test
+    public void testFlowFileStreamPreservesExistingFragmentAttributes() {
+        final TestRunner mergeRunner = TestRunners.newTestRunner(new 
MergeContent());
+        mergeRunner.setProperty(MergeContent.MERGE_FORMAT, 
MergeContent.MergeFormat.FLOWFILE_STREAM_V3);
+        mergeRunner.setProperty(MergeContent.MERGE_STRATEGY, 
MergeContent.MergeStrategy.BIN_PACK);
+        mergeRunner.setProperty(MergeContent.MIN_ENTRIES, "2");
+        mergeRunner.setProperty(MergeContent.MAX_ENTRIES, "2");
+
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put(UnpackContent.FRAGMENT_ID, EXISTING_FRAGMENT_ID);
+        attributes.put(UnpackContent.FRAGMENT_COUNT, "2");
+        attributes.put(UnpackContent.SEGMENT_ORIGINAL_FILENAME, 
EXISTING_SEGMENT_FILENAME);
+        attributes.put(UnpackContent.FRAGMENT_INDEX, FIRST_FRAGMENT_INDEX);
+        mergeRunner.enqueue("Hello ".getBytes(StandardCharsets.UTF_8), 
attributes);
+        attributes.put(UnpackContent.FRAGMENT_INDEX, "2");
+        mergeRunner.enqueue("World".getBytes(StandardCharsets.UTF_8), 
attributes);
+        mergeRunner.run();
+
+        mergeRunner.assertTransferCount(MergeContent.REL_MERGED, 1);
+        final MockFlowFile packaged = 
mergeRunner.getFlowFilesForRelationship(MergeContent.REL_MERGED).getFirst();
+
+        runner.setProperty(UnpackContent.PACKAGING_FORMAT, 
UnpackContent.PackageFormat.FLOWFILE_STREAM_FORMAT_V3);
+        runner.enqueue(packaged);
+        runner.run();
+
+        runner.assertTransferCount(UnpackContent.REL_SUCCESS, 2);
+        runner.assertTransferCount(UnpackContent.REL_FAILURE, 0);
+
+        final List<MockFlowFile> unpacked = 
runner.getFlowFilesForRelationship(UnpackContent.REL_SUCCESS);
+        for (final MockFlowFile flowFile : unpacked) {
+            flowFile.assertAttributeEquals(UnpackContent.FRAGMENT_ID, 
EXISTING_FRAGMENT_ID);
+            flowFile.assertAttributeEquals(UnpackContent.FRAGMENT_COUNT, "2");
+            
flowFile.assertAttributeEquals(UnpackContent.SEGMENT_ORIGINAL_FILENAME, 
EXISTING_SEGMENT_FILENAME);
+        }
+
+        unpacked.get(0).assertAttributeEquals(UnpackContent.FRAGMENT_INDEX, 
FIRST_FRAGMENT_INDEX);
+        unpacked.get(1).assertAttributeEquals(UnpackContent.FRAGMENT_INDEX, 
"2");
+    }
+
     @Test
     public void testTarThenMerge() throws IOException {
         runner.setProperty(UnpackContent.PACKAGING_FORMAT, 
UnpackContent.PackageFormat.TAR_FORMAT);

Reply via email to