This is an automated email from the ASF dual-hosted git repository.
exceptionfactory 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 fcbc059157 NIFI-15541 Fixed Parameter Context binding for new Process
Groups on Version Upgrades (#10844)
fcbc059157 is described below
commit fcbc059157f8b8fa82b1b46d7692c68ef039f1f9
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Feb 6 06:34:58 2026 +0100
NIFI-15541 Fixed Parameter Context binding for new Process Groups on
Version Upgrades (#10844)
Signed-off-by: David Handermann <[email protected]>
---
.../StandardVersionedComponentSynchronizer.java | 59 ++++++-
.../nifi/parameter/ParameterContextNameUtils.java | 141 +++++++++++++++
.../ParameterContextNameCollisionResolver.java | 44 ++---
.../registry/ParameterContextPreservationIT.java | 191 +++++++++++++++++++++
4 files changed, 398 insertions(+), 37 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
index 0d9775fadd..ef54e189c4 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
@@ -92,6 +92,7 @@ import
org.apache.nifi.migration.StandardControllerServiceFactory;
import org.apache.nifi.parameter.Parameter;
import org.apache.nifi.parameter.ParameterContext;
import org.apache.nifi.parameter.ParameterContextManager;
+import org.apache.nifi.parameter.ParameterContextNameUtils;
import org.apache.nifi.parameter.ParameterDescriptor;
import org.apache.nifi.parameter.ParameterProviderConfiguration;
import org.apache.nifi.parameter.ParameterReferenceManager;
@@ -2152,15 +2153,29 @@ public class StandardVersionedComponentSynchronizer
implements VersionedComponen
createMissingParameterProvider(versionedParameterContext,
versionedParameterContext.getParameterProvider(), parameterProviderReferences,
componentIdGenerator);
if (currentParamContext == null) {
// Create a new Parameter Context based on the parameters
provided
- final ParameterContext contextByName =
getParameterContextByName(versionedParameterContext.getName());
final ParameterContext selectedParameterContext;
- if (contextByName == null) {
- final String parameterContextId =
componentIdGenerator.generateUuid(versionedParameterContext.getName(),
- versionedParameterContext.getName(),
versionedParameterContext.getName());
- selectedParameterContext =
createParameterContext(versionedParameterContext, parameterContextId,
versionedParameterContexts,
- parameterProviderReferences,
componentIdGenerator);
+
+ // Check if the parent group has a parameter context that
corresponds to the same
+ // versioned parameter context. If so, we should use the
parent's context to maintain
+ // consistency within this flow instance. This is
important during flow version upgrades
+ // where new child process groups are added - they should
use the same parameter context
+ // as their parent, not a different one that happens to
match by name.
+ final ParameterContext parentParameterContext =
findMatchingParentParameterContext(group, versionedParameterContext.getName());
+ if (parentParameterContext == null) {
+ // Fall back to existing behavior: look up by name or
create new
+ final ParameterContext contextByName =
getParameterContextByName(versionedParameterContext.getName());
+ if (contextByName == null) {
+ final String parameterContextId =
componentIdGenerator.generateUuid(versionedParameterContext.getName(),
+ versionedParameterContext.getName(),
versionedParameterContext.getName());
+ selectedParameterContext =
createParameterContext(versionedParameterContext, parameterContextId,
versionedParameterContexts,
+ parameterProviderReferences,
componentIdGenerator);
+ } else {
+ selectedParameterContext = contextByName;
+ addMissingConfiguration(versionedParameterContext,
selectedParameterContext, versionedParameterContexts,
parameterProviderReferences, componentIdGenerator);
+ }
} else {
- selectedParameterContext = contextByName;
+ selectedParameterContext = parentParameterContext;
+ // Ensure the parent's context has all the parameters
from the versioned context
addMissingConfiguration(versionedParameterContext,
selectedParameterContext, versionedParameterContexts,
parameterProviderReferences, componentIdGenerator);
}
@@ -2173,6 +2188,36 @@ public class StandardVersionedComponentSynchronizer
implements VersionedComponen
}
}
+ /**
+ * Finds a parameter context from the parent group hierarchy that
corresponds to the given versioned
+ * parameter context name. This is used to ensure that when a new child
process group is added during
+ * a flow version upgrade, it uses the same parameter context as its
parent (if they both reference
+ * the same parameter context in the versioned flow), rather than looking
up by name globally which
+ * could result in using a different parameter context with the same base
name.
+ *
+ * @param group the process group being updated
+ * @param versionedParameterContextName the name of the parameter context
in the versioned flow
+ * @return the matching parent parameter context, or null if not found
+ */
+ private ParameterContext findMatchingParentParameterContext(final
ProcessGroup group, final String versionedParameterContextName) {
+ ProcessGroup parent = group.getParent();
+ while (parent != null) {
+ final ParameterContext parentContext =
parent.getParameterContext();
+ if (parentContext != null) {
+ // Check if the parent's context corresponds to the same
versioned parameter context name.
+ // The parent's context name might be the exact name or have a
suffix like " (1)", " (2)", etc.
+ // if it was created during an import with REPLACE strategy.
+ final String parentContextName = parentContext.getName();
+ if (parentContextName.equals(versionedParameterContextName)
+ ||
ParameterContextNameUtils.isNameWithSuffix(parentContextName,
versionedParameterContextName)) {
+ return parentContext;
+ }
+ }
+ parent = parent.getParent();
+ }
+ return null;
+ }
+
private void createMissingParameterProvider(final
VersionedParameterContext versionedParameterContext, final String
parameterProviderId,
final Map<String,
ParameterProviderReference> parameterProviderReferences, final
ComponentIdGenerator componentIdGenerator) {
String parameterProviderIdToSet = parameterProviderId;
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterContextNameUtils.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterContextNameUtils.java
new file mode 100644
index 0000000000..bd4b2e36de
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterContextNameUtils.java
@@ -0,0 +1,141 @@
+/*
+ * 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.parameter;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for handling parameter context name patterns, particularly for
+ * names with numeric suffixes like "Context (1)", "Context (2)", etc.
+ *
+ * <p>When importing versioned flows with the REPLACE parameter context
strategy,
+ * NiFi creates new parameter contexts with suffixed names if a context with
the
+ * same name already exists. This utility provides methods to parse and match
+ * these naming patterns consistently across the codebase.</p>
+ */
+public final class ParameterContextNameUtils {
+
+ private static final String PATTERN_GROUP_NAME = "name";
+ private static final String PATTERN_GROUP_INDEX = "index";
+
+ /**
+ * Pattern that matches parameter context names, optionally with a numeric
suffix.
+ * Examples: "MyContext", "MyContext (1)", "MyContext (2)", "My Context
(10)"
+ */
+ private static final String LINEAGE_FORMAT = "^(?<" + PATTERN_GROUP_NAME +
">.+?)( \\((?<" + PATTERN_GROUP_INDEX + ">[0-9]+)\\))?$";
+ private static final Pattern LINEAGE_PATTERN =
Pattern.compile(LINEAGE_FORMAT);
+
+ /**
+ * Format string for creating parameter context names with a numeric
suffix.
+ * Usage: String.format(NAME_FORMAT, baseName, index) produces "baseName
(index)"
+ */
+ public static final String NAME_FORMAT = "%s (%d)";
+
+ private ParameterContextNameUtils() {
+ // Utility class - prevent instantiation
+ }
+
+ /**
+ * Checks if the given context name matches a base name with a numeric
suffix.
+ * For example, "P (1)" or "P (2)" would match base name "P".
+ *
+ * @param contextName the actual parameter context name to check
+ * @param baseName the base parameter context name (without suffix)
+ * @return true if contextName equals baseName with a suffix like " (n)",
false otherwise
+ */
+ public static boolean isNameWithSuffix(final String contextName, final
String baseName) {
+ if (contextName == null || baseName == null) {
+ return false;
+ }
+
+ final Matcher matcher = LINEAGE_PATTERN.matcher(contextName);
+ if (!matcher.matches()) {
+ return false;
+ }
+
+ final String extractedBaseName = matcher.group(PATTERN_GROUP_NAME);
+ final String indexGroup = matcher.group(PATTERN_GROUP_INDEX);
+
+ // Must have a suffix (index) and the base name must match
+ return indexGroup != null && baseName.equals(extractedBaseName);
+ }
+
+ /**
+ * Extracts the base name from a parameter context name, removing any
numeric suffix.
+ * For example, "MyContext (1)" returns "MyContext", and "MyContext"
returns "MyContext".
+ *
+ * @param name the parameter context name
+ * @return the base name without the suffix, or the original name if no
suffix exists
+ * @throws IllegalArgumentException if the name cannot be parsed
+ */
+ public static String extractBaseName(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Parameter context name cannot
be null");
+ }
+
+ final Matcher matcher = LINEAGE_PATTERN.matcher(name);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Parameter context name \"" +
name + "\" cannot be parsed");
+ }
+
+ return matcher.group(PATTERN_GROUP_NAME);
+ }
+
+ /**
+ * Extracts the suffix index from a parameter context name.
+ * For example, "MyContext (3)" returns 3, and "MyContext" returns -1.
+ *
+ * @param name the parameter context name
+ * @return the suffix index, or -1 if the name has no suffix
+ * @throws IllegalArgumentException if the name cannot be parsed
+ */
+ public static int extractSuffixIndex(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Parameter context name cannot
be null");
+ }
+
+ final Matcher matcher = LINEAGE_PATTERN.matcher(name);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Parameter context name \"" +
name + "\" cannot be parsed");
+ }
+
+ final String indexGroup = matcher.group(PATTERN_GROUP_INDEX);
+ return indexGroup == null ? -1 : Integer.parseInt(indexGroup);
+ }
+
+ /**
+ * Creates a parameter context name with a numeric suffix.
+ *
+ * @param baseName the base name
+ * @param index the suffix index
+ * @return the formatted name, e.g., "baseName (index)"
+ */
+ public static String createNameWithSuffix(final String baseName, final int
index) {
+ return String.format(NAME_FORMAT, baseName, index);
+ }
+
+ /**
+ * Returns the compiled pattern for matching parameter context names with
optional suffixes.
+ * This can be used for more complex matching scenarios.
+ *
+ * @return the compiled Pattern
+ */
+ public static Pattern getLineagePattern() {
+ return LINEAGE_PATTERN;
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java
index a66e4dad37..8da3113c0c 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java
@@ -16,59 +16,43 @@
*/
package org.apache.nifi.web.util;
+import org.apache.nifi.parameter.ParameterContextNameUtils;
import org.apache.nifi.web.api.dto.ParameterContextDTO;
import org.apache.nifi.web.api.entity.ParameterContextEntity;
import java.util.Collection;
import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class ParameterContextNameCollisionResolver {
- private static final String PATTERN_GROUP_NAME = "name";
- private static final String PATTERN_GROUP_INDEX = "index";
-
- private static final String LINEAGE_FORMAT = "^(?<" + PATTERN_GROUP_NAME +
">.+?)( \\((?<" + PATTERN_GROUP_INDEX + ">[0-9]+)\\))?$";
- private static final Pattern LINEAGE_PATTERN =
Pattern.compile(LINEAGE_FORMAT);
-
- private static final String NAME_FORMAT = "%s (%d)";
public String resolveNameCollision(final String
originalParameterContextName, final Collection<ParameterContextEntity>
existingContexts) {
- final Matcher lineageMatcher =
LINEAGE_PATTERN.matcher(originalParameterContextName);
-
- if (!lineageMatcher.matches()) {
- throw new IllegalArgumentException("Existing Parameter Context
name \"(" + originalParameterContextName + "\") cannot be processed");
- }
-
- final String lineName = lineageMatcher.group(PATTERN_GROUP_NAME);
- final String originalIndex = lineageMatcher.group(PATTERN_GROUP_INDEX);
+ final String lineName =
ParameterContextNameUtils.extractBaseName(originalParameterContextName);
+ final int originalIndex =
ParameterContextNameUtils.extractSuffixIndex(originalParameterContextName);
// Candidates cannot be cached because new context might be added
between calls
final Set<ParameterContextDTO> candidates = existingContexts
.stream()
- .map(pc -> pc.getComponent())
+ .map(ParameterContextEntity::getComponent)
.filter(dto -> dto.getName().startsWith(lineName))
.collect(Collectors.toSet());
- int biggestIndex = (originalIndex == null) ? 0 :
Integer.valueOf(originalIndex);
+ int biggestIndex = Math.max(originalIndex, 0);
for (final ParameterContextDTO candidate : candidates) {
- final Matcher matcher =
LINEAGE_PATTERN.matcher(candidate.getName());
-
- if (matcher.matches() &&
lineName.equals(matcher.group(PATTERN_GROUP_NAME))) {
- final String indexGroup = matcher.group(PATTERN_GROUP_INDEX);
-
- if (indexGroup != null) {
- int biggestIndexCandidate = Integer.valueOf(indexGroup);
-
- if (biggestIndexCandidate > biggestIndex) {
- biggestIndex = biggestIndexCandidate;
+ try {
+ final String candidateBaseName =
ParameterContextNameUtils.extractBaseName(candidate.getName());
+ if (lineName.equals(candidateBaseName)) {
+ final int candidateIndex =
ParameterContextNameUtils.extractSuffixIndex(candidate.getName());
+ if (candidateIndex > biggestIndex) {
+ biggestIndex = candidateIndex;
}
}
+ } catch (final IllegalArgumentException ignored) {
+ // Candidate name doesn't match expected pattern, skip it
}
}
- return String.format(NAME_FORMAT, lineName, biggestIndex + 1);
+ return ParameterContextNameUtils.createNameWithSuffix(lineName,
biggestIndex + 1);
}
}
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
new file mode 100644
index 0000000000..41c75af918
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
@@ -0,0 +1,191 @@
+/*
+ * 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.tests.system.registry;
+
+import org.apache.nifi.tests.system.NiFiClientUtil;
+import org.apache.nifi.tests.system.NiFiSystemIT;
+import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.dto.ProcessGroupDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+import org.apache.nifi.web.api.dto.flow.FlowDTO;
+import org.apache.nifi.web.api.entity.FlowRegistryClientEntity;
+import org.apache.nifi.web.api.entity.ParameterContextEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * System test to verify that parameter context bindings are preserved during
versioned flow upgrades
+ * when new process groups are added.
+ *
+ * This test reproduces a bug where:
+ * 1. v1: Process Group A with Parameter Context P, containing only a
Processor X using param1
+ * 2. v2: Process Group A with Parameter Context P, now containing a NEW
Process Group B also attached to P
+ * 3. When checking out v1 twice with "do not keep parameter context":
+ * - First checkout creates A1 with Parameter Context P
+ * - Second checkout creates A2 with Parameter Context P (1) since P
already exists
+ * 4. When upgrading A2 from v1 to v2, the newly added Process Group B
incorrectly gets
+ * bound to P instead of P (1)
+ *
+ * The expectation is that the new Process Group B should be bound to P (1),
the same
+ * parameter context that its parent A2 uses.
+ */
+class ParameterContextPreservationIT extends NiFiSystemIT {
+ private static final String TEST_FLOWS_BUCKET = "test-flows";
+ private static final String PARAMETER_CONTEXT_NAME = "P";
+ private static final String PARAMETER_NAME = "param1";
+ private static final String PARAMETER_VALUE = "value1";
+ private static final String PARAMETER_REFERENCE = "#{" + PARAMETER_NAME +
"}";
+ private static final String GROUP_A_NAME = "A";
+ private static final String GROUP_B_NAME = "B";
+ private static final String FLOW_NAME = "FlowWithParameterContext";
+ private static final String PROCESSOR_TYPE = "GenerateFlowFile";
+ private static final String PROCESSOR_PROPERTY_TEXT = "Text";
+ private static final String RELATIONSHIP_SUCCESS = "success";
+ private static final String VERSION_1 = "1";
+ private static final String VERSION_2 = "2";
+
+ @Test
+ void testNewProcessGroupUsesCorrectParameterContextDuringUpgrade() throws
NiFiClientException, IOException, InterruptedException {
+ final FlowRegistryClientEntity clientEntity = registerClient();
+ final NiFiClientUtil util = getClientUtil();
+
+ // Step 1: Create Parameter Context "P" with param1
+ final ParameterContextEntity paramContextP =
util.createParameterContext(PARAMETER_CONTEXT_NAME, Map.of(PARAMETER_NAME,
PARAMETER_VALUE));
+
+ // Step 2: Create v1 - Process Group A with just a Processor X using
param1
+ final ProcessGroupEntity groupA =
util.createProcessGroup(GROUP_A_NAME, "root");
+ util.setParameterContext(groupA.getId(), paramContextP);
+
+ final ProcessorEntity processorX =
util.createProcessor(PROCESSOR_TYPE, groupA.getId());
+ util.updateProcessorProperties(processorX,
Collections.singletonMap(PROCESSOR_PROPERTY_TEXT, PARAMETER_REFERENCE));
+ util.setAutoTerminatedRelationships(processorX, RELATIONSHIP_SUCCESS);
+
+ // Save as v1
+ final VersionControlInformationEntity vciV1 =
util.startVersionControl(groupA, clientEntity, TEST_FLOWS_BUCKET, FLOW_NAME);
+ final String flowId = vciV1.getVersionControlInformation().getFlowId();
+
+ // Step 3: Create v2 - Add Process Group B inside A, also attached to P
+ final ProcessGroupEntity groupB =
util.createProcessGroup(GROUP_B_NAME, groupA.getId());
+ util.setParameterContext(groupB.getId(), paramContextP);
+
+ // Add a processor in B that uses param1
+ final ProcessorEntity processorInB =
util.createProcessor(PROCESSOR_TYPE, groupB.getId());
+ util.updateProcessorProperties(processorInB,
Collections.singletonMap(PROCESSOR_PROPERTY_TEXT, PARAMETER_REFERENCE));
+ util.setAutoTerminatedRelationships(processorInB,
RELATIONSHIP_SUCCESS);
+
+ // Save as v2
+ final ProcessGroupEntity refreshedGroupA =
getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
+ util.saveFlowVersion(refreshedGroupA, clientEntity, vciV1);
+
+ // Step 4: Clean up original flow
+ final ProcessGroupEntity groupAForStopVc =
getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
+
getNifiClient().getVersionsClient().stopVersionControl(groupAForStopVc);
+ util.deleteAll(groupA.getId());
+ final ProcessGroupEntity groupAToDelete =
getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
+
getNifiClient().getProcessGroupClient().deleteProcessGroup(groupAToDelete);
+
+ final ParameterContextEntity contextToDelete =
getNifiClient().getParamContextClient().getParamContext(paramContextP.getId(),
false);
+
getNifiClient().getParamContextClient().deleteParamContext(paramContextP.getId(),
+ String.valueOf(contextToDelete.getRevision().getVersion()));
+
+ // Step 5: Import v1 FIRST time - creates A1 with Parameter Context "P"
+ final ProcessGroupEntity importedGroup1 =
importFlowWithReplaceParameterContext(
+ clientEntity.getId(), flowId, VERSION_1);
+
+ final ProcessGroupEntity fetchedGroup1 =
getNifiClient().getProcessGroupClient().getProcessGroup(importedGroup1.getId());
+ final String paramContextId1 =
fetchedGroup1.getComponent().getParameterContext().getId();
+
+ // Step 6: Import v1 SECOND time - creates A2 with Parameter Context
"P (1)"
+ final ProcessGroupEntity importedGroup2 =
importFlowWithReplaceParameterContext(
+ clientEntity.getId(), flowId, VERSION_1);
+
+ final ProcessGroupEntity fetchedGroup2 =
getNifiClient().getProcessGroupClient().getProcessGroup(importedGroup2.getId());
+ final String paramContextId2 =
fetchedGroup2.getComponent().getParameterContext().getId();
+
+ // Verify the two imports have DIFFERENT parameter contexts
+ assertNotEquals(paramContextId1, paramContextId2,
+ "Second import should have a different parameter context");
+
+ // Step 7: Upgrade A2 from v1 to v2 (this adds Process Group B)
+ util.changeFlowVersion(importedGroup2.getId(), VERSION_2);
+
+ // Step 8: Verify the NEW Process Group B uses the CORRECT parameter
context
+ final ProcessGroupEntity upgradedGroup2 =
getNifiClient().getProcessGroupClient().getProcessGroup(importedGroup2.getId());
+ final String upgradedA2ParamContextId =
upgradedGroup2.getComponent().getParameterContext().getId();
+
+ // A2 should still use P (1)
+ assertEquals(paramContextId2, upgradedA2ParamContextId,
+ "After upgrade, A2 should still reference its original
parameter context");
+
+ // Get the newly added Process Group B
+ final ProcessGroupEntity newGroupB =
getNestedProcessGroup(upgradedGroup2, GROUP_B_NAME);
+ assertNotNull(newGroupB, "Process Group B should exist after upgrade");
+ assertNotNull(newGroupB.getComponent().getParameterContext(),
+ "Process Group B should have a parameter context assigned");
+
+ final String groupBParamContextId =
newGroupB.getComponent().getParameterContext().getId();
+
+ // The NEW Process Group B should use the SAME parameter context as
its parent A2 (P (1))
+ assertEquals(paramContextId2, groupBParamContextId,
+ "NEW Process Group B should use the same parameter context as
its parent A2, not be bound to the first parameter context P");
+ }
+
+ private ProcessGroupEntity importFlowWithReplaceParameterContext(
+ final String registryClientId,
+ final String flowId,
+ final String version) throws NiFiClientException, IOException {
+
+ final VersionControlInformationDTO vci = new
VersionControlInformationDTO();
+ vci.setBucketId(TEST_FLOWS_BUCKET);
+ vci.setFlowId(flowId);
+ vci.setVersion(version);
+ vci.setRegistryId(registryClientId);
+
+ final ProcessGroupDTO processGroupDto = new ProcessGroupDTO();
+ processGroupDto.setVersionControlInformation(vci);
+
+ final ProcessGroupEntity groupEntity = new ProcessGroupEntity();
+ groupEntity.setComponent(processGroupDto);
+ groupEntity.setRevision(getClientUtil().createNewRevision());
+
+ return
getNifiClient().getProcessGroupClient().createProcessGroup("root", groupEntity,
false);
+ }
+
+ private ProcessGroupEntity getNestedProcessGroup(ProcessGroupEntity
parent, String name) throws NiFiClientException, IOException {
+ final ProcessGroupFlowEntity flowEntity =
getNifiClient().getFlowClient().getProcessGroup(parent.getId());
+ final FlowDTO flowDto = flowEntity.getProcessGroupFlow().getFlow();
+
+ for (ProcessGroupEntity childGroup : flowDto.getProcessGroups()) {
+ if (name.equals(childGroup.getComponent().getName())) {
+ return
getNifiClient().getProcessGroupClient().getProcessGroup(childGroup.getId());
+ }
+ }
+ return null;
+ }
+}