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

markap14 pushed a commit to branch NIFI-15258
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 7a288a57bfc522f2b68562d2052dee44ccab324c
Author: Mark Payne <[email protected]>
AuthorDate: Wed Jan 21 12:25:58 2026 -0500

    NIFI-15451: Added ability for Connectors to retrieve bundles availabl… 
(#10756)
    
    * NIFI-15451: Added ability for Connectors to retrieve bundles available 
for component types and updated VersionedFlowUtils to make use of it to easily 
update Versioned components
    
    * NIFI-15451: Added additional unit tests to VersionedFlowUtils; updates to 
how we compare component versions
    
    * NIFI-15451: Fixed PMD violation
---
 .../connector/util/VersionedFlowUtils.java         | 212 ++++++++++++
 .../connector/util/TestVersionedFlowUtils.java     | 368 +++++++++++++++++++++
 .../server/MockConnectorInitializationContext.java |  19 +-
 ...eworkConnectorInitializationContextBuilder.java |   2 +
 .../connector/StandardComponentBundleLookup.java   |  45 +++
 .../StandardConnectorInitializationContext.java    |  15 +-
 .../apache/nifi/controller/ExtensionBuilder.java   |  11 +-
 .../nifi/controller/flow/StandardFlowManager.java  |   4 +
 .../tests/system/CalculateConnector.java           |   3 -
 9 files changed, 672 insertions(+), 7 deletions(-)

diff --git 
a/nifi-commons/nifi-connector-utils/src/main/java/org/apache/nifi/components/connector/util/VersionedFlowUtils.java
 
b/nifi-commons/nifi-connector-utils/src/main/java/org/apache/nifi/components/connector/util/VersionedFlowUtils.java
index 8eb839a3ba..62c42e107b 100644
--- 
a/nifi-commons/nifi-connector-utils/src/main/java/org/apache/nifi/components/connector/util/VersionedFlowUtils.java
+++ 
b/nifi-commons/nifi-connector-utils/src/main/java/org/apache/nifi/components/connector/util/VersionedFlowUtils.java
@@ -19,12 +19,14 @@ package org.apache.nifi.components.connector.util;
 
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.nifi.components.connector.ComponentBundleLookup;
 import org.apache.nifi.flow.Bundle;
 import org.apache.nifi.flow.ComponentType;
 import org.apache.nifi.flow.ConnectableComponent;
 import org.apache.nifi.flow.ConnectableComponentType;
 import org.apache.nifi.flow.Position;
 import org.apache.nifi.flow.ScheduledState;
+import org.apache.nifi.flow.VersionedConfigurableExtension;
 import org.apache.nifi.flow.VersionedConnection;
 import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedExternalFlow;
@@ -363,4 +365,214 @@ public class VersionedFlowUtils {
             removeUnreferencedControllerServices(childGroup, 
referencedServiceIds);
         }
     }
+
+    /**
+     * Updates all processors and controller services in the given process 
group (and its child groups)
+     * to use the latest available bundle version. See {@link 
#getLatest(List)} for details on how
+     * version comparison is performed.
+     *
+     * @param processGroup the process group containing components to update
+     * @param componentBundleLookup the lookup used to find available bundles 
for each component type
+     */
+    public static void updateToLatestBundles(final VersionedProcessGroup 
processGroup, final ComponentBundleLookup componentBundleLookup) {
+        for (final VersionedProcessor processor : 
processGroup.getProcessors()) {
+            updateToLatestBundle(processor, componentBundleLookup);
+        }
+
+        for (final VersionedControllerService service : 
processGroup.getControllerServices()) {
+            updateToLatestBundle(service, componentBundleLookup);
+        }
+
+        for (final VersionedProcessGroup childGroup : 
processGroup.getProcessGroups()) {
+            updateToLatestBundles(childGroup, componentBundleLookup);
+        }
+    }
+
+    /**
+     * Updates the given processor to use the latest available bundle version.
+     * See {@link #getLatest(List)} for details on how version comparison is 
performed.
+     *
+     * @param processor the processor to update
+     * @param componentBundleLookup the lookup used to find available bundles 
for the processor type
+     */
+    public static void updateToLatestBundle(final VersionedProcessor 
processor, final ComponentBundleLookup componentBundleLookup) {
+        final Bundle latest = getLatestBundle(processor, 
componentBundleLookup);
+        processor.setBundle(latest);
+    }
+
+    /**
+     * Updates the given controller service to use the latest available bundle 
version.
+     * See {@link #getLatest(List)} for details on how version comparison is 
performed.
+     *
+     * @param service the controller service to update
+     * @param componentBundleLookup the lookup used to find available bundles 
for the service type
+     */
+    public static void updateToLatestBundle(final VersionedControllerService 
service, final ComponentBundleLookup componentBundleLookup) {
+        final Bundle latest = getLatestBundle(service, componentBundleLookup);
+        service.setBundle(latest);
+    }
+
+    private static Bundle getLatestBundle(final VersionedConfigurableExtension 
versionedComponent, final ComponentBundleLookup componentBundleLookup) {
+        final String type = versionedComponent.getType();
+        final List<Bundle> bundles = 
componentBundleLookup.getAvailableBundles(type);
+        final List<Bundle> includingExisting = new ArrayList<>(bundles);
+        if (versionedComponent.getBundle() != null && 
!includingExisting.contains(versionedComponent.getBundle())) {
+            includingExisting.add(versionedComponent.getBundle());
+        }
+
+        return getLatest(includingExisting);
+    }
+
+    /**
+     * Returns the bundle with the latest version from the given list.
+     *
+     * <p>Version comparison follows these rules:</p>
+     * <ol>
+     *   <li>Versions are split by dots (e.g., "2.1.0" becomes ["2", "1", 
"0"]).
+     *       This also supports calendar-date formats (e.g., 
"2026.01.01").</li>
+     *   <li>Each part is compared numerically when possible; numeric parts 
are considered
+     *       newer than non-numeric parts (e.g., "2.0.0" &gt; "2.0.next")</li>
+     *   <li>When base versions are equal, qualifiers are compared with the 
following precedence
+     *       (highest to lowest):
+     *       <ul>
+     *         <li>Release (no qualifier)</li>
+     *         <li>RC (Release Candidate) - e.g., "-RC1", "-RC2"</li>
+     *         <li>M (Milestone) - e.g., "-M1", "-M2"</li>
+     *         <li>Other/unknown qualifiers</li>
+     *         <li>SNAPSHOT</li>
+     *       </ul>
+     *   </li>
+     *   <li>Within the same qualifier type, numeric suffixes are compared
+     *       (e.g., "2.0.0-RC2" &gt; "2.0.0-RC1", "2.0.0-M4" &gt; 
"2.0.0-M1")</li>
+     * </ol>
+     *
+     * <p>Examples of version ordering (highest to lowest):</p>
+     * <ul>
+     *   <li>2.0.0 &gt; 2.0.0-RC2 &gt; 2.0.0-RC1 &gt; 2.0.0-M4 &gt; 2.0.0-M1 
&gt; 2.0.0-SNAPSHOT</li>
+     *   <li>2.1.0-SNAPSHOT &gt; 2.0.0 (higher base version wins)</li>
+     *   <li>2026.01.01 &gt; 2025.12.31 (calendar-date format)</li>
+     * </ul>
+     *
+     * @param bundles the list of bundles to compare
+     * @return the bundle with the latest version, or null if the list is empty
+     */
+    public static Bundle getLatest(final List<Bundle> bundles) {
+        Bundle latest = null;
+        for (final Bundle bundle : bundles) {
+            if (latest == null || compareVersion(bundle.getVersion(), 
latest.getVersion()) > 0) {
+                latest = bundle;
+            }
+        }
+
+        return latest;
+    }
+
+    private static int compareVersion(final String v1, final String v2) {
+        final String baseVersion1 = getBaseVersion(v1);
+        final String baseVersion2 = getBaseVersion(v2);
+
+        final String[] parts1 = baseVersion1.split("\\.");
+        final String[] parts2 = baseVersion2.split("\\.");
+
+        final int length = Math.max(parts1.length, parts2.length);
+        for (int i = 0; i < length; i++) {
+            final String part1Str = i < parts1.length ? parts1[i] : "0";
+            final String part2Str = i < parts2.length ? parts2[i] : "0";
+
+            final int comparison = compareVersionPart(part1Str, part2Str);
+            if (comparison != 0) {
+                return comparison;
+            }
+        }
+
+        // Base versions are equal; compare qualifiers
+        final String qualifier1 = getQualifier(v1);
+        final String qualifier2 = getQualifier(v2);
+        return compareQualifiers(qualifier1, qualifier2);
+    }
+
+    private static int compareQualifiers(final String qualifier1, final String 
qualifier2) {
+        final int rank1 = getQualifierRank(qualifier1);
+        final int rank2 = getQualifierRank(qualifier2);
+
+        if (rank1 != rank2) {
+            return Integer.compare(rank1, rank2);
+        }
+
+        // Same qualifier type; compare numeric suffixes (e.g., RC2 > RC1, M4 
> M3)
+        final int num1 = getQualifierNumber(qualifier1);
+        final int num2 = getQualifierNumber(qualifier2);
+        return Integer.compare(num1, num2);
+    }
+
+    private static int getQualifierRank(final String qualifier) {
+        if (qualifier == null || qualifier.isEmpty()) {
+            return 4;
+        } else if (qualifier.startsWith("RC")) {
+            return 3;
+        } else if (qualifier.startsWith("M")) {
+            return 2;
+        } else if (qualifier.equals("SNAPSHOT")) {
+            return 0;
+        } else {
+            return 1;
+        }
+    }
+
+    private static int getQualifierNumber(final String qualifier) {
+        if (qualifier == null || qualifier.isEmpty()) {
+            return 0;
+        }
+
+        final StringBuilder digits = new StringBuilder();
+        for (int i = 0; i < qualifier.length(); i++) {
+            final char c = qualifier.charAt(i);
+            if (Character.isDigit(c)) {
+                digits.append(c);
+            }
+        }
+
+        if (digits.isEmpty()) {
+            return 0;
+        }
+
+        try {
+            return Integer.parseInt(digits.toString());
+        } catch (final NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    private static String getQualifier(final String version) {
+        final int qualifierIndex = version.indexOf('-');
+        return qualifierIndex > 0 ? version.substring(qualifierIndex + 1) : 
null;
+    }
+
+    private static int compareVersionPart(final String part1, final String 
part2) {
+        final Integer num1 = parseVersionPart(part1);
+        final Integer num2 = parseVersionPart(part2);
+
+        if (num1 != null && num2 != null) {
+            return Integer.compare(num1, num2);
+        } else if (num1 != null) {
+            return 1;
+        } else if (num2 != null) {
+            return -1;
+        } else {
+            return part1.compareTo(part2);
+        }
+    }
+
+    private static Integer parseVersionPart(final String part) {
+        try {
+            return Integer.parseInt(part);
+        } catch (final NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private static String getBaseVersion(final String version) {
+        final int qualifierIndex = version.indexOf('-');
+        return qualifierIndex > 0 ? version.substring(0, qualifierIndex) : 
version;
+    }
 }
diff --git 
a/nifi-commons/nifi-connector-utils/src/test/java/org/apache/nifi/components/connector/util/TestVersionedFlowUtils.java
 
b/nifi-commons/nifi-connector-utils/src/test/java/org/apache/nifi/components/connector/util/TestVersionedFlowUtils.java
new file mode 100644
index 0000000000..5ff0149d5a
--- /dev/null
+++ 
b/nifi-commons/nifi-connector-utils/src/test/java/org/apache/nifi/components/connector/util/TestVersionedFlowUtils.java
@@ -0,0 +1,368 @@
+/*
+ * 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.nifi.components.connector.util;
+
+import org.apache.nifi.components.connector.ComponentBundleLookup;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.ComponentType;
+import org.apache.nifi.flow.Position;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class TestVersionedFlowUtils {
+
+    @Nested
+    class GetLatest {
+        @Test
+        void testMinorVersion() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0.0"),
+                new Bundle("group", "artifact", "1.2.0")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("1.2.0", latestBundle.getVersion());
+        }
+
+        @Test
+        void testMajorVersion() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0.0"),
+                new Bundle("group", "artifact", "2.0.0")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("2.0.0", latestBundle.getVersion());
+        }
+
+        @Test
+        void testMoreDigitsLater() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0"),
+                new Bundle("group", "artifact", "1.0.1")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("1.0.1", latestBundle.getVersion());
+        }
+
+        @Test
+        void testMoreDigitsFirst() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0.1"),
+                new Bundle("group", "artifact", "1.0")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("1.0.1", latestBundle.getVersion());
+        }
+
+        @Test
+        void testWithSnapshotAndSameVersion() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0.0-SNAPSHOT"),
+                new Bundle("group", "artifact", "1.0.0")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("1.0.0", latestBundle.getVersion());
+        }
+
+        @Test
+        void testWithSnapshotAndDifferentVersion() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0.1-SNAPSHOT"),
+                new Bundle("group", "artifact", "1.0.0")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("1.0.1-SNAPSHOT", latestBundle.getVersion());
+        }
+
+        @Test
+        void testEmptyList() {
+            final List<Bundle> bundles = List.of();
+            assertNull(VersionedFlowUtils.getLatest(bundles));
+        }
+
+        @Test
+        void testNonNumericVersionPart() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "4.0.0"),
+                new Bundle("group", "artifact", "4.0.next")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("4.0.0", latestBundle.getVersion());
+        }
+
+        @Test
+        void testFullyNonNumericVersionVsNumeric() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "1.0.0"),
+                new Bundle("group", "artifact", "undefined")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("1.0.0", latestBundle.getVersion());
+        }
+
+        @Test
+        void testTwoFullyNonNumericVersions() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "undefined"),
+                new Bundle("group", "artifact", "unknown")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("unknown", latestBundle.getVersion());
+        }
+
+        @Test
+        void testQualifierOrdering() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "2.0.0-SNAPSHOT"),
+                new Bundle("group", "artifact", "2.0.0-M1"),
+                new Bundle("group", "artifact", "2.0.0-M4"),
+                new Bundle("group", "artifact", "2.0.0-RC1"),
+                new Bundle("group", "artifact", "2.0.0-RC2"),
+                new Bundle("group", "artifact", "2.0.0")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("2.0.0", latestBundle.getVersion());
+        }
+
+        @Test
+        void testQualifierOrderingWithoutRelease() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "2.0.0-SNAPSHOT"),
+                new Bundle("group", "artifact", "2.0.0-M1"),
+                new Bundle("group", "artifact", "2.0.0-M4"),
+                new Bundle("group", "artifact", "2.0.0-RC1"),
+                new Bundle("group", "artifact", "2.0.0-RC2")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("2.0.0-RC2", latestBundle.getVersion());
+        }
+
+        @Test
+        void testMilestoneVersionsOrdering() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "2.0.0-M1"),
+                new Bundle("group", "artifact", "2.0.0-M4"),
+                new Bundle("group", "artifact", "2.0.0-SNAPSHOT")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("2.0.0-M4", latestBundle.getVersion());
+        }
+
+        @Test
+        void testCalendarDateFormat() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "2025.12.31"),
+                new Bundle("group", "artifact", "2026.01.01")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("2026.01.01", latestBundle.getVersion());
+        }
+
+        @Test
+        void testCalendarDateFormatWithBuildNumber() {
+            final List<Bundle> bundles = List.of(
+                new Bundle("group", "artifact", "2025.12.31.999"),
+                new Bundle("group", "artifact", "2026.01.01.451")
+            );
+
+            final Bundle latestBundle = VersionedFlowUtils.getLatest(bundles);
+            assertNotNull(latestBundle);
+            assertEquals("2026.01.01.451", latestBundle.getVersion());
+        }
+    }
+
+    @Nested
+    class UpdateToLatestBundles {
+        private static final String PROCESSOR_TYPE = 
"org.apache.nifi.processors.TestProcessor";
+        private static final String SERVICE_TYPE = 
"org.apache.nifi.services.TestService";
+
+        @Test
+        void testLookupReturnsOneOlderBundle() {
+            final VersionedProcessGroup group = createProcessGroup();
+            final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, new Bundle("group", 
"artifact", "2.0.0"), "Test Processor", new Position(0, 0));
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(PROCESSOR_TYPE)).thenReturn(List.of(new 
Bundle("group", "artifact", "1.0.0")));
+
+            VersionedFlowUtils.updateToLatestBundles(group, lookup);
+
+            assertEquals("2.0.0", processor.getBundle().getVersion());
+        }
+
+        @Test
+        void testLookupReturnsOneNewerBundle() {
+            final VersionedProcessGroup group = createProcessGroup();
+            final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, new Bundle("group", 
"artifact", "2.0.0"), "Test Processor", new Position(0, 0));
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(PROCESSOR_TYPE)).thenReturn(List.of(new 
Bundle("group", "artifact", "3.0.0")));
+
+            VersionedFlowUtils.updateToLatestBundles(group, lookup);
+
+            assertEquals("3.0.0", processor.getBundle().getVersion());
+        }
+
+        @Test
+        void testLookupReturnsMultipleBundlesIncludingSameOlderAndNewer() {
+            final VersionedProcessGroup group = createProcessGroup();
+            final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, new Bundle("group", 
"artifact", "2.0.0"), "Test Processor", new Position(0, 0));
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(PROCESSOR_TYPE)).thenReturn(List.of(
+                new Bundle("group", "artifact", "1.0.0"),
+                new Bundle("group", "artifact", "2.0.0"),
+                new Bundle("group", "artifact", "3.0.0"),
+                new Bundle("group", "artifact", "4.0.0")
+            ));
+
+            VersionedFlowUtils.updateToLatestBundles(group, lookup);
+
+            assertEquals("4.0.0", processor.getBundle().getVersion());
+        }
+
+        @Test
+        void testLookupReturnsNoBundles() {
+            final VersionedProcessGroup group = createProcessGroup();
+            final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, new Bundle("group", 
"artifact", "2.0.0"), "Test Processor", new Position(0, 0));
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(PROCESSOR_TYPE)).thenReturn(List.of());
+
+            VersionedFlowUtils.updateToLatestBundles(group, lookup);
+
+            assertEquals("2.0.0", processor.getBundle().getVersion());
+        }
+
+        @Test
+        void testControllerServiceUpdatedToNewerBundle() {
+            final VersionedProcessGroup group = createProcessGroup();
+            final VersionedControllerService service = 
VersionedFlowUtils.addControllerService(group, SERVICE_TYPE, new 
Bundle("group", "artifact", "1.5.0"), "Test Service");
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(SERVICE_TYPE)).thenReturn(List.of(new 
Bundle("group", "artifact", "2.5.0")));
+
+            VersionedFlowUtils.updateToLatestBundles(group, lookup);
+
+            assertEquals("2.5.0", service.getBundle().getVersion());
+        }
+
+        @Test
+        void testNestedProcessorUpdatedToNewerBundle() {
+            final VersionedProcessGroup rootGroup = createProcessGroup();
+            final VersionedProcessGroup childGroup = 
createChildProcessGroup(rootGroup, "child-group-id");
+            final VersionedProcessor nestedProcessor = 
VersionedFlowUtils.addProcessor(childGroup, PROCESSOR_TYPE, new Bundle("group", 
"artifact", "1.0.0"), "Nested Processor", new Position(0, 0));
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(PROCESSOR_TYPE)).thenReturn(List.of(new 
Bundle("group", "artifact", "5.0.0")));
+
+            VersionedFlowUtils.updateToLatestBundles(rootGroup, lookup);
+
+            assertEquals("5.0.0", nestedProcessor.getBundle().getVersion());
+        }
+
+        @Test
+        void testLookupReturnsVersionsWithQualifiers() {
+            final VersionedProcessGroup group = createProcessGroup();
+            final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, new Bundle("group", 
"artifact", "2.0.0"), "Test Processor", new Position(0, 0));
+
+            final ComponentBundleLookup lookup = 
mock(ComponentBundleLookup.class);
+            
when(lookup.getAvailableBundles(PROCESSOR_TYPE)).thenReturn(List.of(
+                new Bundle("group", "artifact", "2.0.0"),
+                new Bundle("group", "artifact", "2.0.0-SNAPSHOT"),
+                new Bundle("group", "artifact", "2.0.0-M1"),
+                new Bundle("group", "artifact", "2.0.0-RC1")
+            ));
+
+            VersionedFlowUtils.updateToLatestBundles(group, lookup);
+
+            assertEquals("2.0.0", processor.getBundle().getVersion());
+        }
+
+        private VersionedProcessGroup createProcessGroup() {
+            final VersionedProcessGroup group = new VersionedProcessGroup();
+            group.setIdentifier("test-group-id");
+            group.setName("Test Process Group");
+            group.setProcessors(new HashSet<>());
+            group.setProcessGroups(new HashSet<>());
+            group.setControllerServices(new HashSet<>());
+            group.setConnections(new HashSet<>());
+            group.setInputPorts(new HashSet<>());
+            group.setOutputPorts(new HashSet<>());
+            group.setFunnels(new HashSet<>());
+            group.setLabels(new HashSet<>());
+            group.setComponentType(ComponentType.PROCESS_GROUP);
+            return group;
+        }
+
+        private VersionedProcessGroup createChildProcessGroup(final 
VersionedProcessGroup parent, final String identifier) {
+            final VersionedProcessGroup childGroup = new 
VersionedProcessGroup();
+            childGroup.setIdentifier(identifier);
+            childGroup.setName("Child Process Group");
+            childGroup.setGroupIdentifier(parent.getIdentifier());
+            childGroup.setProcessors(new HashSet<>());
+            childGroup.setProcessGroups(new HashSet<>());
+            childGroup.setControllerServices(new HashSet<>());
+            childGroup.setConnections(new HashSet<>());
+            childGroup.setInputPorts(new HashSet<>());
+            childGroup.setOutputPorts(new HashSet<>());
+            childGroup.setFunnels(new HashSet<>());
+            childGroup.setLabels(new HashSet<>());
+            childGroup.setComponentType(ComponentType.PROCESS_GROUP);
+            parent.getProcessGroups().add(childGroup);
+            return childGroup;
+        }
+    }
+
+}
diff --git 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
index 93cb6fd44a..d4af4dcbb2 100644
--- 
a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
+++ 
b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockConnectorInitializationContext.java
@@ -18,12 +18,13 @@
 package org.apache.nifi.mock.connector.server;
 
 import org.apache.nifi.asset.AssetManager;
-import 
org.apache.nifi.components.connector.FrameworkConnectorInitializationContextBuilder;
+import org.apache.nifi.components.connector.ComponentBundleLookup;
 import org.apache.nifi.components.connector.FlowUpdateException;
 import 
org.apache.nifi.components.connector.FrameworkConnectorInitializationContext;
+import 
org.apache.nifi.components.connector.FrameworkConnectorInitializationContextBuilder;
 import org.apache.nifi.components.connector.FrameworkFlowContext;
-import org.apache.nifi.components.connector.secrets.SecretsManager;
 import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.secrets.SecretsManager;
 import org.apache.nifi.flow.VersionedExternalFlow;
 import org.apache.nifi.flow.VersionedProcessGroup;
 import org.apache.nifi.flow.VersionedProcessor;
@@ -36,6 +37,7 @@ public class MockConnectorInitializationContext implements 
FrameworkConnectorIni
     private final ComponentLog componentLog;
     private final SecretsManager secretsManager;
     private final AssetManager assetManager;
+    private final ComponentBundleLookup componentBundleLookup;
 
     private final MockExtensionMapper mockExtensionMapper;
 
@@ -46,6 +48,7 @@ public class MockConnectorInitializationContext implements 
FrameworkConnectorIni
         this.secretsManager = builder.secretsManager;
         this.assetManager = builder.assetManager;
         this.mockExtensionMapper = builder.mockExtensionMapper;
+        this.componentBundleLookup = builder.componentBundleLookup;
     }
 
 
@@ -64,6 +67,11 @@ public class MockConnectorInitializationContext implements 
FrameworkConnectorIni
         return componentLog;
     }
 
+    @Override
+    public ComponentBundleLookup getComponentBundleLookup() {
+        return componentBundleLookup;
+    }
+
     @Override
     public SecretsManager getSecretsManager() {
         return secretsManager;
@@ -118,6 +126,7 @@ public class MockConnectorInitializationContext implements 
FrameworkConnectorIni
         private ComponentLog componentLog;
         private SecretsManager secretsManager;
         private AssetManager assetManager;
+        private ComponentBundleLookup componentBundleLookup;
 
         public Builder(final MockExtensionMapper mockExtensionMapper) {
             this.mockExtensionMapper = mockExtensionMapper;
@@ -147,6 +156,12 @@ public class MockConnectorInitializationContext implements 
FrameworkConnectorIni
             return this;
         }
 
+        @Override
+        public Builder componentBundleLookup(final ComponentBundleLookup 
bundleLookup) {
+            this.componentBundleLookup = bundleLookup;
+            return this;
+        }
+
         @Override
         public Builder assetManager(final AssetManager assetManager) {
             this.assetManager = assetManager;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/FrameworkConnectorInitializationContextBuilder.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/FrameworkConnectorInitializationContextBuilder.java
index 030e333a84..9ef92941a8 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/FrameworkConnectorInitializationContextBuilder.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/FrameworkConnectorInitializationContextBuilder.java
@@ -33,5 +33,7 @@ public interface 
FrameworkConnectorInitializationContextBuilder {
 
     FrameworkConnectorInitializationContextBuilder 
secretsManager(SecretsManager secretsManager);
 
+    FrameworkConnectorInitializationContextBuilder 
componentBundleLookup(ComponentBundleLookup bundleLookup);
+
     FrameworkConnectorInitializationContext build();
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardComponentBundleLookup.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardComponentBundleLookup.java
new file mode 100644
index 0000000000..84d664d80b
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardComponentBundleLookup.java
@@ -0,0 +1,45 @@
+/*
+ * 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.nifi.components.connector;
+
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.nar.ExtensionManager;
+
+import java.util.List;
+
+public class StandardComponentBundleLookup implements ComponentBundleLookup {
+    private final ExtensionManager extensionManager;
+
+    public StandardComponentBundleLookup(final ExtensionManager 
extensionManager) {
+        this.extensionManager = extensionManager;
+    }
+
+    @Override
+    public List<Bundle> getAvailableBundles(final String componentType) {
+        final List<org.apache.nifi.bundle.Bundle> bundles = 
extensionManager.getBundles(componentType);
+        return bundles.stream()
+            .map(this::convertBundle)
+            .toList();
+    }
+
+    private Bundle convertBundle(final org.apache.nifi.bundle.Bundle bundle) {
+        final BundleCoordinate coordinate = 
bundle.getBundleDetails().getCoordinate();
+        return new Bundle(coordinate.getGroup(), coordinate.getId(), 
coordinate.getVersion());
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorInitializationContext.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorInitializationContext.java
index c31cad237f..ab2dcbf399 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorInitializationContext.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorInitializationContext.java
@@ -30,6 +30,7 @@ public class StandardConnectorInitializationContext 
implements FrameworkConnecto
     private final ComponentLog componentLog;
     private final SecretsManager secretsManager;
     private final AssetManager assetManager;
+    private final ComponentBundleLookup componentBundleLookup;
 
 
     private StandardConnectorInitializationContext(final Builder builder) {
@@ -38,9 +39,9 @@ public class StandardConnectorInitializationContext 
implements FrameworkConnecto
         this.componentLog = builder.componentLog;
         this.secretsManager = builder.secretsManager;
         this.assetManager = builder.assetManager;
+        this.componentBundleLookup = builder.componentBundleLookup;
     }
 
-
     @Override
     public String getIdentifier() {
         return identifier;
@@ -56,6 +57,11 @@ public class StandardConnectorInitializationContext 
implements FrameworkConnecto
         return componentLog;
     }
 
+    @Override
+    public ComponentBundleLookup getComponentBundleLookup() {
+        return componentBundleLookup;
+    }
+
     @Override
     public SecretsManager getSecretsManager() {
         return secretsManager;
@@ -90,6 +96,7 @@ public class StandardConnectorInitializationContext 
implements FrameworkConnecto
         private ComponentLog componentLog;
         private SecretsManager secretsManager;
         private AssetManager assetManager;
+        private ComponentBundleLookup componentBundleLookup;
 
         @Override
         public Builder identifier(final String identifier) {
@@ -121,6 +128,12 @@ public class StandardConnectorInitializationContext 
implements FrameworkConnecto
             return this;
         }
 
+        @Override
+        public Builder componentBundleLookup(final ComponentBundleLookup 
bundleLookup) {
+            this.componentBundleLookup = bundleLookup;
+            return this;
+        }
+
         @Override
         public StandardConnectorInitializationContext build() {
             return new StandardConnectorInitializationContext(this);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
index 6d985e297d..64f06ded7c 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
@@ -28,6 +28,7 @@ import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.ComponentBundleLookup;
 import org.apache.nifi.components.connector.ConfigurationStep;
 import org.apache.nifi.components.connector.Connector;
 import org.apache.nifi.components.connector.ConnectorDetails;
@@ -147,6 +148,7 @@ public class ExtensionBuilder {
     private ConnectorStateTransition connectorStateTransition;
     private FrameworkConnectorInitializationContextBuilder 
connectorInitializationContextBuilder;
     private ConnectorValidationTrigger connectorValidationTrigger;
+    private ComponentBundleLookup componentBundleLookup;
 
     public ExtensionBuilder type(final String type) {
         this.type = type;
@@ -281,6 +283,12 @@ public class ExtensionBuilder {
         return this;
     }
 
+    public ExtensionBuilder componentBundleLookup(final ComponentBundleLookup 
componentBundleLookup) {
+        this.componentBundleLookup = componentBundleLookup;
+        return this;
+    }
+
+
     public ProcessorNode buildProcessor() {
         requireNonNull(identifier, "Processor ID");
         requireNonNull(type, "Processor Type");
@@ -574,7 +582,7 @@ public class ExtensionBuilder {
         connectorNode.initializeConnector(initContext);
 
         return connectorNode;
-   }
+    }
 
     private void initializeDefaultValues(final Connector connector, final 
FrameworkFlowContext flowContext) {
         try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, connector.getClass(), 
identifier)) {
@@ -605,6 +613,7 @@ public class ExtensionBuilder {
             .componentLog(componentLog)
             
.secretsManager(flowController.getConnectorRepository().getSecretsManager())
             .assetManager(flowController.getConnectorAssetManager())
+            .componentBundleLookup(componentBundleLookup)
             .build();
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
index d3874eb5cb..3826c9a371 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
@@ -27,12 +27,14 @@ import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.ComponentBundleLookup;
 import org.apache.nifi.components.connector.Connector;
 import org.apache.nifi.components.connector.ConnectorNode;
 import org.apache.nifi.components.connector.ConnectorRepository;
 import org.apache.nifi.components.connector.ConnectorStateTransition;
 import org.apache.nifi.components.connector.FlowContextFactory;
 import org.apache.nifi.components.connector.ProcessGroupFactory;
+import org.apache.nifi.components.connector.StandardComponentBundleLookup;
 import 
org.apache.nifi.components.connector.StandardConnectorConfigurationContext;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.ConnectableType;
@@ -763,6 +765,7 @@ public class StandardFlowManager extends 
AbstractFlowManager implements FlowMana
 
         final ProcessGroupFactory processGroupFactory = groupId -> 
createProcessGroup(groupId, id);
         final FlowContextFactory flowContextFactory = new 
FlowControllerFlowContextFactory(flowController, managedRootGroup, 
activeConfigurationContext, processGroupFactory);
+        final ComponentBundleLookup componentBundleLookup = new 
StandardComponentBundleLookup(extensionManager);
 
         final ConnectorNode connectorNode = new ExtensionBuilder()
             .identifier(id)
@@ -776,6 +779,7 @@ public class StandardFlowManager extends 
AbstractFlowManager implements FlowMana
             .connectorStateTransition(stateTransition)
             
.connectorInitializationContextBuilder(flowController.getConnectorRepository().createInitializationContextBuilder())
             
.connectorValidationTrigger(flowController.getConnectorValidationTrigger())
+            .componentBundleLookup(componentBundleLookup)
             .buildConnector(firstTimeAdded);
 
         // Establish the Connector as the parent authorizable of the managed 
root group
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/CalculateConnector.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/CalculateConnector.java
index db43577038..d8763e0587 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/CalculateConnector.java
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/CalculateConnector.java
@@ -100,9 +100,6 @@ public class CalculateConnector extends AbstractConnector {
         private Calculation calculation;
         private int result;
 
-        public CalculatedResult() {
-        }
-
         public Calculation getCalculation() {
             return calculation;
         }


Reply via email to