exceptionfactory commented on code in PR #10327:
URL: https://github.com/apache/nifi/pull/10327#discussion_r2369816063


##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()

Review Comment:
   I recommend declaring the Set `final` and setting it in an `else` block.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {

Review Comment:
   The negated `isPresent()` can be changed to `isEmpty()`



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));

Review Comment:
   Can `toSet()` be used instead of a new HashSet?



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -58,6 +71,12 @@ public class FlowDifferenceFilters {
      * @return <code>true</code> if the change is an environment-specific 
change, <code>false</code> otherwise
      */
     public static boolean isEnvironmentalChange(final FlowDifference 
difference, final VersionedProcessGroup localGroup, final FlowManager 
flowManager) {
+        return isEnvironmentalChange(difference, localGroup, flowManager, 
EnvironmentalChangeContext.empty());
+    }
+
+    public static boolean isEnvironmentalChange(final FlowDifference 
difference, final VersionedProcessGroup localGroup, final FlowManager 
flowManager,
+                                                final 
EnvironmentalChangeContext context) {
+        final EnvironmentalChangeContext evaluatedContext = context == null ? 
EnvironmentalChangeContext.empty() : context;

Review Comment:
   I recommend requiring the `EnvironmentalChangeContext` for this method, 
instead of substituting an `empty()` context.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED

Review Comment:
   It looks like this should be an `else if` instead of a standalone `if` 
statement.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -112,6 +133,15 @@ private static ComponentNode getComponent(final 
FlowManager flowManager, final C
 
     }
 
+    private static ComponentNode getComponent(final FlowManager flowManager, 
final VersionedComponent component) {
+        if (!(component instanceof InstantiatedVersionedComponent)) {
+            return null;
+        }
+
+        final InstantiatedVersionedComponent instantiatedComponent = 
(InstantiatedVersionedComponent) component;
+        return getComponent(flowManager, component.getComponentType(), 
instantiatedComponent.getInstanceIdentifier());

Review Comment:
   Minor implementation note, it looks like this could be adjusted to use 
`instanceof InstantiatedVersionedComponent instantiated) { ...` and otherwise 
return `null`, instead of the short-circuit return and casting.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));
+        }
+
+        final Set<FlowDifference> parameterizedPropertyRenameDifferences = new 
HashSet<>();
+        for (final Map.Entry<String, List<PropertyDiffInfo>> entry : 
parameterizationRemovalsByComponent.entrySet()) {
+            final String componentId = entry.getKey();
+            final List<PropertyDiffInfo> removals = entry.getValue();
+            final List<PropertyDiffInfo> additions = new 
ArrayList<>(parameterizedAddsByComponent.getOrDefault(componentId, 
Collections.emptyList()));
+            if (additions.isEmpty()) {
+                continue;
+            }
+
+            for (final PropertyDiffInfo removalInfo : removals) {
+                final Optional<String> removalValue = 
removalInfo.propertyValue();
+                if (removalValue.isPresent() && 
!isParameterReference(removalValue.get())) {
+                    continue;
+                }
+
+                PropertyDiffInfo matchingAddition = null;
+                for (final Iterator<PropertyDiffInfo> iterator = 
additions.iterator(); iterator.hasNext();) {
+                    final PropertyDiffInfo additionInfo = iterator.next();
+                    final Optional<String> additionValue = 
additionInfo.propertyValue();
+                    if (additionValue.isPresent() && 
!isParameterReference(additionValue.get())) {
+                        continue;
+                    }
+
+                    if (valuesMatch(removalValue, additionValue)) {
+                        matchingAddition = additionInfo;
+                        iterator.remove();
+                        break;
+                    }
+                }
+
+                if (matchingAddition != null) {
+                    
parameterizedPropertyRenameDifferences.add(removalInfo.difference());
+                    
parameterizedPropertyRenameDifferences.add(matchingAddition.difference());
+                }
+            }
+        }
+
+        if (serviceIdsWithMatchingAdditions.isEmpty() && 
parameterizedPropertyRenameDifferences.isEmpty()) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions, 
parameterizedPropertyRenameDifferences);

Review Comment:
   Minor, but since these are close to the end of the method, I recommend 
declaring a `final EnvironmentChangeContext` and setting the value as opposed 
to having multiple returns.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));
+        }
+
+        final Set<FlowDifference> parameterizedPropertyRenameDifferences = new 
HashSet<>();
+        for (final Map.Entry<String, List<PropertyDiffInfo>> entry : 
parameterizationRemovalsByComponent.entrySet()) {
+            final String componentId = entry.getKey();
+            final List<PropertyDiffInfo> removals = entry.getValue();
+            final List<PropertyDiffInfo> additions = new 
ArrayList<>(parameterizedAddsByComponent.getOrDefault(componentId, 
Collections.emptyList()));
+            if (additions.isEmpty()) {
+                continue;
+            }
+
+            for (final PropertyDiffInfo removalInfo : removals) {
+                final Optional<String> removalValue = 
removalInfo.propertyValue();
+                if (removalValue.isPresent() && 
!isParameterReference(removalValue.get())) {
+                    continue;
+                }
+
+                PropertyDiffInfo matchingAddition = null;
+                for (final Iterator<PropertyDiffInfo> iterator = 
additions.iterator(); iterator.hasNext();) {
+                    final PropertyDiffInfo additionInfo = iterator.next();
+                    final Optional<String> additionValue = 
additionInfo.propertyValue();
+                    if (additionValue.isPresent() && 
!isParameterReference(additionValue.get())) {
+                        continue;
+                    }
+
+                    if (valuesMatch(removalValue, additionValue)) {
+                        matchingAddition = additionInfo;
+                        iterator.remove();
+                        break;
+                    }
+                }
+
+                if (matchingAddition != null) {
+                    
parameterizedPropertyRenameDifferences.add(removalInfo.difference());
+                    
parameterizedPropertyRenameDifferences.add(matchingAddition.difference());
+                }
+            }
+        }
+
+        if (serviceIdsWithMatchingAdditions.isEmpty() && 
parameterizedPropertyRenameDifferences.isEmpty()) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions, 
parameterizedPropertyRenameDifferences);
+    }
+
+    public static boolean isControllerServiceCreatedForNewProperty(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return isControllerServiceCreatedForNewPropertyInternal(difference, 
context == null ? EnvironmentalChangeContext.empty() : context);
+    }
+
+    private static boolean 
isControllerServiceCreatedForNewPropertyInternal(final FlowDifference 
difference, final EnvironmentalChangeContext context) {
+        if (context.serviceIdsCreatedForNewProperties().isEmpty()) {
+            return false;
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.PROPERTY_ADDED) {
+            final Object valueB = difference.getValueB();
+            if (valueB instanceof String) {
+                return 
context.serviceIdsCreatedForNewProperties().contains(valueB);
+            }

Review Comment:
   The `instanceof` check seems unnecessary since it would be handled by 
`contains`, unless `valueB` can be `null`.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));
+        }
+
+        final Set<FlowDifference> parameterizedPropertyRenameDifferences = new 
HashSet<>();
+        for (final Map.Entry<String, List<PropertyDiffInfo>> entry : 
parameterizationRemovalsByComponent.entrySet()) {
+            final String componentId = entry.getKey();
+            final List<PropertyDiffInfo> removals = entry.getValue();
+            final List<PropertyDiffInfo> additions = new 
ArrayList<>(parameterizedAddsByComponent.getOrDefault(componentId, 
Collections.emptyList()));
+            if (additions.isEmpty()) {
+                continue;
+            }
+
+            for (final PropertyDiffInfo removalInfo : removals) {
+                final Optional<String> removalValue = 
removalInfo.propertyValue();
+                if (removalValue.isPresent() && 
!isParameterReference(removalValue.get())) {
+                    continue;
+                }
+
+                PropertyDiffInfo matchingAddition = null;
+                for (final Iterator<PropertyDiffInfo> iterator = 
additions.iterator(); iterator.hasNext();) {
+                    final PropertyDiffInfo additionInfo = iterator.next();
+                    final Optional<String> additionValue = 
additionInfo.propertyValue();
+                    if (additionValue.isPresent() && 
!isParameterReference(additionValue.get())) {
+                        continue;
+                    }
+
+                    if (valuesMatch(removalValue, additionValue)) {
+                        matchingAddition = additionInfo;
+                        iterator.remove();
+                        break;
+                    }
+                }
+
+                if (matchingAddition != null) {
+                    
parameterizedPropertyRenameDifferences.add(removalInfo.difference());
+                    
parameterizedPropertyRenameDifferences.add(matchingAddition.difference());
+                }
+            }
+        }
+
+        if (serviceIdsWithMatchingAdditions.isEmpty() && 
parameterizedPropertyRenameDifferences.isEmpty()) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions, 
parameterizedPropertyRenameDifferences);
+    }
+
+    public static boolean isControllerServiceCreatedForNewProperty(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return isControllerServiceCreatedForNewPropertyInternal(difference, 
context == null ? EnvironmentalChangeContext.empty() : context);
+    }
+
+    private static boolean 
isControllerServiceCreatedForNewPropertyInternal(final FlowDifference 
difference, final EnvironmentalChangeContext context) {
+        if (context.serviceIdsCreatedForNewProperties().isEmpty()) {
+            return false;
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.PROPERTY_ADDED) {
+            final Object valueB = difference.getValueB();
+            if (valueB instanceof String) {
+                return 
context.serviceIdsCreatedForNewProperties().contains(valueB);
+            }
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.COMPONENT_ADDED) {
+            final String serviceIdentifier = 
extractControllerServiceIdentifier(difference);
+            return serviceIdentifier != null && 
context.serviceIdsCreatedForNewProperties().contains(serviceIdentifier);
+        }
+
+        return false;
+    }
+
+    private static String extractControllerServiceIdentifier(final 
FlowDifference difference) {
+        final String identifierFromComponentB = 
extractControllerServiceIdentifier(difference.getComponentB());
+        if (identifierFromComponentB != null) {
+            return identifierFromComponentB;
+        }
+
+        return extractControllerServiceIdentifier(difference.getComponentA());
+    }
+
+    private static String extractControllerServiceIdentifier(final 
VersionedComponent component) {
+        if (component instanceof InstantiatedVersionedControllerService) {
+            final InstantiatedVersionedControllerService 
instantiatedControllerService = (InstantiatedVersionedControllerService) 
component;
+            final String instanceIdentifier = 
instantiatedControllerService.getInstanceIdentifier();
+            if (instanceIdentifier != null) {
+                return instanceIdentifier;
+            }
+        }
+
+        if (component instanceof VersionedControllerService) {
+            return component.getIdentifier();
+        }
+
+        return null;
+    }
+
+    public static boolean isPropertyParameterizationRename(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return (context == null || 
context.parameterizedPropertyRenames().isEmpty()) ? false : 
context.parameterizedPropertyRenames().contains(difference);

Review Comment:
   See note on `null` possibility for `context`.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));
+        }
+
+        final Set<FlowDifference> parameterizedPropertyRenameDifferences = new 
HashSet<>();
+        for (final Map.Entry<String, List<PropertyDiffInfo>> entry : 
parameterizationRemovalsByComponent.entrySet()) {
+            final String componentId = entry.getKey();
+            final List<PropertyDiffInfo> removals = entry.getValue();
+            final List<PropertyDiffInfo> additions = new 
ArrayList<>(parameterizedAddsByComponent.getOrDefault(componentId, 
Collections.emptyList()));
+            if (additions.isEmpty()) {
+                continue;
+            }
+
+            for (final PropertyDiffInfo removalInfo : removals) {
+                final Optional<String> removalValue = 
removalInfo.propertyValue();
+                if (removalValue.isPresent() && 
!isParameterReference(removalValue.get())) {
+                    continue;
+                }
+
+                PropertyDiffInfo matchingAddition = null;
+                for (final Iterator<PropertyDiffInfo> iterator = 
additions.iterator(); iterator.hasNext();) {
+                    final PropertyDiffInfo additionInfo = iterator.next();
+                    final Optional<String> additionValue = 
additionInfo.propertyValue();
+                    if (additionValue.isPresent() && 
!isParameterReference(additionValue.get())) {
+                        continue;
+                    }
+
+                    if (valuesMatch(removalValue, additionValue)) {
+                        matchingAddition = additionInfo;
+                        iterator.remove();
+                        break;
+                    }
+                }
+
+                if (matchingAddition != null) {
+                    
parameterizedPropertyRenameDifferences.add(removalInfo.difference());
+                    
parameterizedPropertyRenameDifferences.add(matchingAddition.difference());
+                }
+            }
+        }
+
+        if (serviceIdsWithMatchingAdditions.isEmpty() && 
parameterizedPropertyRenameDifferences.isEmpty()) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions, 
parameterizedPropertyRenameDifferences);
+    }
+
+    public static boolean isControllerServiceCreatedForNewProperty(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return isControllerServiceCreatedForNewPropertyInternal(difference, 
context == null ? EnvironmentalChangeContext.empty() : context);
+    }
+
+    private static boolean 
isControllerServiceCreatedForNewPropertyInternal(final FlowDifference 
difference, final EnvironmentalChangeContext context) {
+        if (context.serviceIdsCreatedForNewProperties().isEmpty()) {
+            return false;
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.PROPERTY_ADDED) {
+            final Object valueB = difference.getValueB();
+            if (valueB instanceof String) {
+                return 
context.serviceIdsCreatedForNewProperties().contains(valueB);
+            }
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.COMPONENT_ADDED) {
+            final String serviceIdentifier = 
extractControllerServiceIdentifier(difference);
+            return serviceIdentifier != null && 
context.serviceIdsCreatedForNewProperties().contains(serviceIdentifier);
+        }
+
+        return false;
+    }
+
+    private static String extractControllerServiceIdentifier(final 
FlowDifference difference) {
+        final String identifierFromComponentB = 
extractControllerServiceIdentifier(difference.getComponentB());
+        if (identifierFromComponentB != null) {
+            return identifierFromComponentB;
+        }
+
+        return extractControllerServiceIdentifier(difference.getComponentA());
+    }
+
+    private static String extractControllerServiceIdentifier(final 
VersionedComponent component) {
+        if (component instanceof InstantiatedVersionedControllerService) {
+            final InstantiatedVersionedControllerService 
instantiatedControllerService = (InstantiatedVersionedControllerService) 
component;
+            final String instanceIdentifier = 
instantiatedControllerService.getInstanceIdentifier();
+            if (instanceIdentifier != null) {
+                return instanceIdentifier;
+            }
+        }
+
+        if (component instanceof VersionedControllerService) {
+            return component.getIdentifier();
+        }
+
+        return null;
+    }
+
+    public static boolean isPropertyParameterizationRename(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return (context == null || 
context.parameterizedPropertyRenames().isEmpty()) ? false : 
context.parameterizedPropertyRenames().contains(difference);
+    }
+
+    private static Optional<String> getComponentInstanceIdentifier(final 
FlowDifference difference) {
+        final Optional<String> identifierB = 
getComponentInstanceIdentifier(difference.getComponentB());
+        if (identifierB.isPresent()) {
+            return identifierB;
+        }
+
+        return getComponentInstanceIdentifier(difference.getComponentA());
+    }
+
+    private static Optional<String> getComponentInstanceIdentifier(final 
VersionedComponent component) {
+        if (component == null) {
+            return Optional.empty();
+        }
+
+        if (component instanceof InstantiatedVersionedComponent) {
+            final String instanceId = ((InstantiatedVersionedComponent) 
component).getInstanceIdentifier();
+            if (instanceId != null) {
+                return Optional.of(instanceId);
+            }
+        }
+
+        return Optional.ofNullable(component.getIdentifier());
+    }
+
+    private static Optional<String> getParameterReferenceValue(final 
FlowDifference difference, final boolean fromComponentA) {
+        final VersionedComponent primaryComponent = fromComponentA ? 
difference.getComponentA() : difference.getComponentB();
+        final VersionedComponent secondaryComponent = fromComponentA ? 
difference.getComponentB() : difference.getComponentA();
+
+        final Map<String, String> primaryProperties = 
getProperties(primaryComponent);
+        if (primaryProperties.isEmpty()) {
+            return Optional.empty();

Review Comment:
   This method is a bit longer, making the multiple return statements a bit 
more complex. What do you think about refactoring to a single return?



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));
+        }
+
+        final Set<FlowDifference> parameterizedPropertyRenameDifferences = new 
HashSet<>();
+        for (final Map.Entry<String, List<PropertyDiffInfo>> entry : 
parameterizationRemovalsByComponent.entrySet()) {
+            final String componentId = entry.getKey();
+            final List<PropertyDiffInfo> removals = entry.getValue();
+            final List<PropertyDiffInfo> additions = new 
ArrayList<>(parameterizedAddsByComponent.getOrDefault(componentId, 
Collections.emptyList()));
+            if (additions.isEmpty()) {
+                continue;
+            }
+
+            for (final PropertyDiffInfo removalInfo : removals) {
+                final Optional<String> removalValue = 
removalInfo.propertyValue();
+                if (removalValue.isPresent() && 
!isParameterReference(removalValue.get())) {
+                    continue;
+                }
+
+                PropertyDiffInfo matchingAddition = null;
+                for (final Iterator<PropertyDiffInfo> iterator = 
additions.iterator(); iterator.hasNext();) {
+                    final PropertyDiffInfo additionInfo = iterator.next();
+                    final Optional<String> additionValue = 
additionInfo.propertyValue();
+                    if (additionValue.isPresent() && 
!isParameterReference(additionValue.get())) {
+                        continue;
+                    }
+
+                    if (valuesMatch(removalValue, additionValue)) {
+                        matchingAddition = additionInfo;
+                        iterator.remove();
+                        break;
+                    }
+                }
+
+                if (matchingAddition != null) {
+                    
parameterizedPropertyRenameDifferences.add(removalInfo.difference());
+                    
parameterizedPropertyRenameDifferences.add(matchingAddition.difference());
+                }
+            }
+        }
+
+        if (serviceIdsWithMatchingAdditions.isEmpty() && 
parameterizedPropertyRenameDifferences.isEmpty()) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions, 
parameterizedPropertyRenameDifferences);
+    }
+
+    public static boolean isControllerServiceCreatedForNewProperty(final 
FlowDifference difference, final EnvironmentalChangeContext context) {

Review Comment:
   Can `context` be null? Similar to other public methods, I recommend using 
`Objects.requireNonNull()`



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED
+                    || difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED) {
+
+                final Optional<String> propertyNameOptional = 
difference.getFieldName();
+                final Optional<String> componentIdOptional = 
getComponentInstanceIdentifier(difference);
+
+                if (!propertyNameOptional.isPresent() || 
!componentIdOptional.isPresent()) {
+                    continue;
+                }
+
+                final String componentId = componentIdOptional.get();
+                final Optional<String> propertyValue = 
difference.getDifferenceType() == DifferenceType.PROPERTY_PARAMETERIZED
+                        ? getParameterReferenceValue(difference, false)
+                        : getParameterReferenceValue(difference, true);
+
+                final PropertyDiffInfo diffInfo = new 
PropertyDiffInfo(propertyValue, difference);
+
+                if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_PARAMETERIZED) {
+                    parameterizedAddsByComponent.computeIfAbsent(componentId, 
key -> new ArrayList<>()).add(diffInfo);
+                } else {
+                    
parameterizationRemovalsByComponent.computeIfAbsent(componentId, key -> new 
ArrayList<>()).add(diffInfo);
+                }
+            }
+        }
+
+        Set<String> serviceIdsWithMatchingAdditions = Collections.emptySet();
+        if (!serviceIdsReferencedByNewProperties.isEmpty()) {
+            serviceIdsWithMatchingAdditions = differences.stream()
+                    .filter(diff -> diff.getDifferenceType() == 
DifferenceType.COMPONENT_ADDED)
+                    
.map(FlowDifferenceFilters::extractControllerServiceIdentifier)
+                    .filter(Objects::nonNull)
+                    .filter(serviceIdsReferencedByNewProperties::contains)
+                    .collect(Collectors.toCollection(HashSet::new));
+        }
+
+        final Set<FlowDifference> parameterizedPropertyRenameDifferences = new 
HashSet<>();
+        for (final Map.Entry<String, List<PropertyDiffInfo>> entry : 
parameterizationRemovalsByComponent.entrySet()) {
+            final String componentId = entry.getKey();
+            final List<PropertyDiffInfo> removals = entry.getValue();
+            final List<PropertyDiffInfo> additions = new 
ArrayList<>(parameterizedAddsByComponent.getOrDefault(componentId, 
Collections.emptyList()));
+            if (additions.isEmpty()) {
+                continue;
+            }
+
+            for (final PropertyDiffInfo removalInfo : removals) {
+                final Optional<String> removalValue = 
removalInfo.propertyValue();
+                if (removalValue.isPresent() && 
!isParameterReference(removalValue.get())) {
+                    continue;
+                }
+
+                PropertyDiffInfo matchingAddition = null;
+                for (final Iterator<PropertyDiffInfo> iterator = 
additions.iterator(); iterator.hasNext();) {
+                    final PropertyDiffInfo additionInfo = iterator.next();
+                    final Optional<String> additionValue = 
additionInfo.propertyValue();
+                    if (additionValue.isPresent() && 
!isParameterReference(additionValue.get())) {
+                        continue;
+                    }
+
+                    if (valuesMatch(removalValue, additionValue)) {
+                        matchingAddition = additionInfo;
+                        iterator.remove();
+                        break;
+                    }
+                }
+
+                if (matchingAddition != null) {
+                    
parameterizedPropertyRenameDifferences.add(removalInfo.difference());
+                    
parameterizedPropertyRenameDifferences.add(matchingAddition.difference());
+                }
+            }
+        }
+
+        if (serviceIdsWithMatchingAdditions.isEmpty() && 
parameterizedPropertyRenameDifferences.isEmpty()) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        return new EnvironmentalChangeContext(serviceIdsWithMatchingAdditions, 
parameterizedPropertyRenameDifferences);
+    }
+
+    public static boolean isControllerServiceCreatedForNewProperty(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return isControllerServiceCreatedForNewPropertyInternal(difference, 
context == null ? EnvironmentalChangeContext.empty() : context);
+    }
+
+    private static boolean 
isControllerServiceCreatedForNewPropertyInternal(final FlowDifference 
difference, final EnvironmentalChangeContext context) {
+        if (context.serviceIdsCreatedForNewProperties().isEmpty()) {
+            return false;
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.PROPERTY_ADDED) {
+            final Object valueB = difference.getValueB();
+            if (valueB instanceof String) {
+                return 
context.serviceIdsCreatedForNewProperties().contains(valueB);
+            }
+        }
+
+        if (difference.getDifferenceType() == DifferenceType.COMPONENT_ADDED) {
+            final String serviceIdentifier = 
extractControllerServiceIdentifier(difference);
+            return serviceIdentifier != null && 
context.serviceIdsCreatedForNewProperties().contains(serviceIdentifier);
+        }
+
+        return false;
+    }
+
+    private static String extractControllerServiceIdentifier(final 
FlowDifference difference) {
+        final String identifierFromComponentB = 
extractControllerServiceIdentifier(difference.getComponentB());
+        if (identifierFromComponentB != null) {
+            return identifierFromComponentB;
+        }
+
+        return extractControllerServiceIdentifier(difference.getComponentA());
+    }
+
+    private static String extractControllerServiceIdentifier(final 
VersionedComponent component) {
+        if (component instanceof InstantiatedVersionedControllerService) {
+            final InstantiatedVersionedControllerService 
instantiatedControllerService = (InstantiatedVersionedControllerService) 
component;
+            final String instanceIdentifier = 
instantiatedControllerService.getInstanceIdentifier();
+            if (instanceIdentifier != null) {
+                return instanceIdentifier;
+            }
+        }
+
+        if (component instanceof VersionedControllerService) {
+            return component.getIdentifier();
+        }
+
+        return null;
+    }
+
+    public static boolean isPropertyParameterizationRename(final 
FlowDifference difference, final EnvironmentalChangeContext context) {
+        return (context == null || 
context.parameterizedPropertyRenames().isEmpty()) ? false : 
context.parameterizedPropertyRenames().contains(difference);
+    }
+
+    private static Optional<String> getComponentInstanceIdentifier(final 
FlowDifference difference) {
+        final Optional<String> identifierB = 
getComponentInstanceIdentifier(difference.getComponentB());
+        if (identifierB.isPresent()) {
+            return identifierB;
+        }
+
+        return getComponentInstanceIdentifier(difference.getComponentA());
+    }
+
+    private static Optional<String> getComponentInstanceIdentifier(final 
VersionedComponent component) {
+        if (component == null) {
+            return Optional.empty();
+        }
+
+        if (component instanceof InstantiatedVersionedComponent) {
+            final String instanceId = ((InstantiatedVersionedComponent) 
component).getInstanceIdentifier();
+            if (instanceId != null) {
+                return Optional.of(instanceId);
+            }
+        }
+
+        return Optional.ofNullable(component.getIdentifier());
+    }
+
+    private static Optional<String> getParameterReferenceValue(final 
FlowDifference difference, final boolean fromComponentA) {
+        final VersionedComponent primaryComponent = fromComponentA ? 
difference.getComponentA() : difference.getComponentB();
+        final VersionedComponent secondaryComponent = fromComponentA ? 
difference.getComponentB() : difference.getComponentA();
+
+        final Map<String, String> primaryProperties = 
getProperties(primaryComponent);
+        if (primaryProperties.isEmpty()) {
+            return Optional.empty();
+        }
+
+        final Map<String, String> secondaryProperties = 
getProperties(secondaryComponent);
+
+        final Optional<String> fieldNameOptional = difference.getFieldName();
+        if (fieldNameOptional.isPresent()) {
+            final String fieldName = fieldNameOptional.get();
+            final String propertyValue = primaryProperties.get(fieldName);
+            if (propertyValue != null) {
+                return Optional.of(propertyValue);
+            }
+        }
+
+        // Fallback: find a property unique to the primary component whose 
value is a parameter reference.
+        for (Map.Entry<String, String> entry : primaryProperties.entrySet()) {
+            final String propertyName = entry.getKey();
+            final String propertyValue = entry.getValue();
+            if (!isParameterReference(propertyValue)) {
+                continue;
+            }
+
+            if (!secondaryProperties.containsKey(propertyName)) {
+                return Optional.of(propertyValue);
+            }
+        }
+
+        return Optional.empty();
+    }
+
+    private static Map<String, String> getProperties(final VersionedComponent 
component) {
+        if (component == null) {

Review Comment:
   Recommend refactoring this method to a single return an use of `if ... else 
if ...`



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/FlowDifferenceFilters.java:
##########
@@ -547,4 +577,302 @@ private static boolean isParameterContextChange(final 
FlowDifference flowDiffere
     private static boolean isLogFileSuffixChange(final FlowDifference 
flowDifference) {
         return flowDifference.getDifferenceType() == 
DifferenceType.LOG_FILE_SUFFIX_CHANGED;
     }
+
+    public static EnvironmentalChangeContext 
buildEnvironmentalChangeContext(final Collection<FlowDifference> differences, 
final FlowManager flowManager) {
+        if (differences == null || differences.isEmpty() || flowManager == 
null) {
+            return EnvironmentalChangeContext.empty();
+        }
+
+        final Set<String> serviceIdsReferencedByNewProperties = new 
HashSet<>();
+        final Map<String, List<PropertyDiffInfo>> parameterizedAddsByComponent 
= new HashMap<>();
+        final Map<String, List<PropertyDiffInfo>> 
parameterizationRemovalsByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : differences) {
+            if (difference.getDifferenceType() == 
DifferenceType.PROPERTY_ADDED) {
+                final ComponentNode componentNode = 
Optional.ofNullable(getComponent(flowManager, difference.getComponentB()))
+                        .orElseGet(() -> getComponent(flowManager, 
difference.getComponentA()));
+                if (componentNode != null) {
+                    final Optional<String> fieldNameOptional = 
difference.getFieldName();
+                    if (fieldNameOptional.isPresent()) {
+                        final PropertyDescriptor propertyDescriptor = 
componentNode.getPropertyDescriptor(fieldNameOptional.get());
+                        if (propertyDescriptor != null && 
!propertyDescriptor.isDynamic() && 
propertyDescriptor.getControllerServiceDefinition() != null) {
+                            final Object valueB = difference.getValueB();
+                            if (valueB instanceof String) {
+                                
serviceIdsReferencedByNewProperties.add((String) valueB);

Review Comment:
   Minor adjustment to remove the need for casting:
   ```suggestion
                               if (valueB instanceof String serviceId) {
                                   
serviceIdsReferencedByNewProperties.add(serviceId);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to