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


##########
nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/MigrateToControllerService.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+import java.util.Set;
+
+@CapabilityDescription("""
+        Pre-upgrade shape of a processor that keeps its store location in a 
plain property.
+        The post-upgrade shape of the same processor, in the alternate-config 
extensions bundle, migrates that property into a Controller Service.
+        """)
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class MigrateToControllerService extends AbstractProcessor {
+
+    static final PropertyDescriptor STORE_NAME = new 
PropertyDescriptor.Builder()
+            .name("store-name")
+            .displayName("Store Name")
+            .required(true)

Review Comment:
   The `displayName` is not needed and should be removed.
   
   ```suggestion
               .name("Store Name")
   ```



##########
nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/MigrateToControllerService.java:
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+import java.util.Set;
+
+@CapabilityDescription("""
+        Pre-upgrade shape of a processor that keeps its store location in a 
plain property.
+        The post-upgrade shape of the same processor, in the alternate-config 
extensions bundle, migrates that property into a Controller Service.
+        """)
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+public class MigrateToControllerService extends AbstractProcessor {
+
+    static final PropertyDescriptor STORE_NAME = new 
PropertyDescriptor.Builder()
+            .name("store-name")
+            .displayName("Store Name")
+            .required(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    static final Relationship REL_SUCCESS = new 
Relationship.Builder().name("success").build();
+
+    private static final List<PropertyDescriptor> PROPERTIES = 
List.of(STORE_NAME);
+    private static final Set<Relationship> RELATIONSHIPS = Set.of(REL_SUCCESS);
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTIES;
+    }
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+        context.yield();

Review Comment:
   Is there a reason for calling `yield()` instead of having an empty body?



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java:
##########
@@ -1169,9 +1172,228 @@ private void removeMissingRpg(final ProcessGroup group, 
final VersionedProcessGr
         removeMissingComponents(group, proposed, rpgsByVersionedId, 
VersionedProcessGroup::getRemoteProcessGroups, 
ProcessGroup::removeRemoteProcessGroup);
     }
 
+    /**
+     * Assigns a proposed versioned id to a Controller Service created by 
property migration.
+     * The service must have exactly one referencer.
+     * The proposed counterpart of that referencer must point at a Controller 
Service of the same type.
+     * If those do not hold, the service stays unversioned.
+     * A proposed id is assigned to at most one local service.
+     */
+    private void assignVersionedIdsToMigrationCreatedControllerServices(final 
ProcessGroup group, final VersionedProcessGroup proposed) {
+        final Collection<ControllerServiceNode> groupServices = 
group.getControllerServices(false);
+        if (groupServices.isEmpty()) {
+            return;
+        }
+
+        final List<ControllerServiceNode> migrationCreatedServices = 
getUnversionedMigrationCreatedServices(groupServices);
+        if (migrationCreatedServices.isEmpty()) {
+            return;
+        }
+
+        final Set<String> claimedVersionedIds = 
getClaimedVersionedIds(groupServices);
+
+        final Set<VersionedControllerService> proposedControllerServices = 
Objects.requireNonNullElse(proposed.getControllerServices(), Set.of());
+        final Set<VersionedProcessor> proposedProcessors = 
Objects.requireNonNullElse(proposed.getProcessors(), Set.of());
+        final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId = 
indexByVersionedId(proposedControllerServices, proposedProcessors);
+
+        for (final ControllerServiceNode localService : 
orderByReferencerChain(migrationCreatedServices)) {
+            assignMatchingVersionedId(group, localService, 
claimedVersionedIds, proposedComponentsByVersionedId);
+        }
+    }
+
+    private List<ControllerServiceNode> 
getUnversionedMigrationCreatedServices(final Collection<ControllerServiceNode> 
groupServices) {
+        final List<ControllerServiceNode> migrationCreatedServices = new 
ArrayList<>();
+        for (final ControllerServiceNode localService : groupServices) {
+            if (localService.getVersionedComponentId().isEmpty() && 
isMigrationCreated(localService)) {
+                migrationCreatedServices.add(localService);
+            }
+        }
+        return migrationCreatedServices;
+    }
+
+    private Set<String> getClaimedVersionedIds(final 
Collection<ControllerServiceNode> groupServices) {
+        final Set<String> claimedVersionedIds = 
HashSet.newHashSet(groupServices.size());
+        for (final ControllerServiceNode localService : groupServices) {
+            
localService.getVersionedComponentId().ifPresent(claimedVersionedIds::add);
+        }
+        return claimedVersionedIds;
+    }
+
+    private void assignMatchingVersionedId(
+            final ProcessGroup group,
+            final ControllerServiceNode localService,
+            final Set<String> claimedVersionedIds,
+            final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId
+    ) {
+        final ComponentNode referencer = getSoleReferencer(localService);
+        if (referencer == null) {
+            LOG.debug("Leaving {} in {} unversioned because it is not 
referenced by exactly one component", localService, group);
+            return;
+        }
+
+        final VersionedConfigurableExtension proposedReferencer = 
getProposedReferencer(referencer, proposedComponentsByVersionedId);
+        if (proposedReferencer == null) {
+            LOG.debug("Leaving {} in {} unversioned because its referencer {} 
has no counterpart in the proposed flow", localService, group, referencer);
+            return;
+        }
+
+        final ProposedControllerServiceMatch match = 
findMatchingProposedControllerService(localService, referencer, 
proposedReferencer, proposedComponentsByVersionedId, group);
+        if (match == null) {
+            LOG.debug("Leaving {} in {} unversioned because no proposed 
Controller Service matches the referencing property of {}", localService, 
group, referencer);
+            return;
+        }
+
+        if (!claimedVersionedIds.add(match.versionedId())) {
+            LOG.debug("Leaving {} in {} unversioned because versioned id {} is 
already used by another Controller Service", localService, group, 
match.versionedId());
+            return;
+        }
+
+        localService.setVersionedComponentId(match.versionedId());
+        updatedVersionedComponentIds.add(match.versionedId());
+        LOG.info("Matched {} in {} to the Controller Service with versioned id 
{} that the proposed flow declares, based on the {} property of {}",
+            localService, group, match.versionedId(), match.propertyName(), 
referencer);
+    }
+
+    private List<ControllerServiceNode> orderByReferencerChain(final 
List<ControllerServiceNode> migrationCreatedServices) {

Review Comment:
   The reason behind the ordering is not quite clear, it would be helpful to 
add a comment that explains why the ordering is needed



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java:
##########
@@ -1169,9 +1172,228 @@ private void removeMissingRpg(final ProcessGroup group, 
final VersionedProcessGr
         removeMissingComponents(group, proposed, rpgsByVersionedId, 
VersionedProcessGroup::getRemoteProcessGroups, 
ProcessGroup::removeRemoteProcessGroup);
     }
 
+    /**
+     * Assigns a proposed versioned id to a Controller Service created by 
property migration.
+     * The service must have exactly one referencer.
+     * The proposed counterpart of that referencer must point at a Controller 
Service of the same type.
+     * If those do not hold, the service stays unversioned.
+     * A proposed id is assigned to at most one local service.
+     */
+    private void assignVersionedIdsToMigrationCreatedControllerServices(final 
ProcessGroup group, final VersionedProcessGroup proposed) {
+        final Collection<ControllerServiceNode> groupServices = 
group.getControllerServices(false);
+        if (groupServices.isEmpty()) {
+            return;
+        }
+
+        final List<ControllerServiceNode> migrationCreatedServices = 
getUnversionedMigrationCreatedServices(groupServices);
+        if (migrationCreatedServices.isEmpty()) {
+            return;
+        }
+
+        final Set<String> claimedVersionedIds = 
getClaimedVersionedIds(groupServices);
+
+        final Set<VersionedControllerService> proposedControllerServices = 
Objects.requireNonNullElse(proposed.getControllerServices(), Set.of());
+        final Set<VersionedProcessor> proposedProcessors = 
Objects.requireNonNullElse(proposed.getProcessors(), Set.of());
+        final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId = 
indexByVersionedId(proposedControllerServices, proposedProcessors);
+
+        for (final ControllerServiceNode localService : 
orderByReferencerChain(migrationCreatedServices)) {
+            assignMatchingVersionedId(group, localService, 
claimedVersionedIds, proposedComponentsByVersionedId);
+        }
+    }
+
+    private List<ControllerServiceNode> 
getUnversionedMigrationCreatedServices(final Collection<ControllerServiceNode> 
groupServices) {
+        final List<ControllerServiceNode> migrationCreatedServices = new 
ArrayList<>();
+        for (final ControllerServiceNode localService : groupServices) {
+            if (localService.getVersionedComponentId().isEmpty() && 
isMigrationCreated(localService)) {
+                migrationCreatedServices.add(localService);
+            }
+        }
+        return migrationCreatedServices;
+    }
+
+    private Set<String> getClaimedVersionedIds(final 
Collection<ControllerServiceNode> groupServices) {
+        final Set<String> claimedVersionedIds = 
HashSet.newHashSet(groupServices.size());
+        for (final ControllerServiceNode localService : groupServices) {
+            
localService.getVersionedComponentId().ifPresent(claimedVersionedIds::add);
+        }
+        return claimedVersionedIds;
+    }
+
+    private void assignMatchingVersionedId(
+            final ProcessGroup group,
+            final ControllerServiceNode localService,
+            final Set<String> claimedVersionedIds,
+            final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId
+    ) {
+        final ComponentNode referencer = getSoleReferencer(localService);
+        if (referencer == null) {
+            LOG.debug("Leaving {} in {} unversioned because it is not 
referenced by exactly one component", localService, group);
+            return;
+        }
+
+        final VersionedConfigurableExtension proposedReferencer = 
getProposedReferencer(referencer, proposedComponentsByVersionedId);
+        if (proposedReferencer == null) {
+            LOG.debug("Leaving {} in {} unversioned because its referencer {} 
has no counterpart in the proposed flow", localService, group, referencer);
+            return;
+        }
+
+        final ProposedControllerServiceMatch match = 
findMatchingProposedControllerService(localService, referencer, 
proposedReferencer, proposedComponentsByVersionedId, group);
+        if (match == null) {
+            LOG.debug("Leaving {} in {} unversioned because no proposed 
Controller Service matches the referencing property of {}", localService, 
group, referencer);
+            return;
+        }
+
+        if (!claimedVersionedIds.add(match.versionedId())) {
+            LOG.debug("Leaving {} in {} unversioned because versioned id {} is 
already used by another Controller Service", localService, group, 
match.versionedId());
+            return;
+        }
+
+        localService.setVersionedComponentId(match.versionedId());
+        updatedVersionedComponentIds.add(match.versionedId());
+        LOG.info("Matched {} in {} to the Controller Service with versioned id 
{} that the proposed flow declares, based on the {} property of {}",
+            localService, group, match.versionedId(), match.propertyName(), 
referencer);
+    }
+
+    private List<ControllerServiceNode> orderByReferencerChain(final 
List<ControllerServiceNode> migrationCreatedServices) {
+        final BitSet visited = new BitSet(migrationCreatedServices.size());
+        final List<ControllerServiceNode> ordered = new 
ArrayList<>(migrationCreatedServices.size());
+
+        for (int i = 0; i < migrationCreatedServices.size(); i++) {
+            appendReferencerChain(i, migrationCreatedServices, visited, 
ordered);
+        }
+
+        return ordered;
+    }
+
+    private void appendReferencerChain(
+            final int index,
+            final List<ControllerServiceNode> migrationCreatedServices,
+            final BitSet visited,
+            final List<ControllerServiceNode> ordered
+    ) {
+        if (visited.get(index)) {
+            return;
+        }
+        visited.set(index);
+
+        final ControllerServiceNode localService = 
migrationCreatedServices.get(index);
+        final ComponentNode referencer = getSoleReferencer(localService);
+        if (referencer instanceof ControllerServiceNode referencingService && 
isMigrationCreated(referencingService)) {
+            final int referencerIndex = 
migrationCreatedServices.indexOf(referencingService);
+            if (referencerIndex >= 0) {
+                // Visit the creator first so we can assign the versioned id 
to the creator first.
+                appendReferencerChain(referencerIndex, 
migrationCreatedServices, visited, ordered);
+            }
+        }
+
+        ordered.add(localService);
+    }
+
+    private boolean isMigrationCreated(final ControllerServiceNode service) {
+        return 
StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT.equals(service.getComments());
+    }
+
+    private Map<String, VersionedConfigurableExtension> indexByVersionedId(
+            final Collection<? extends VersionedConfigurableExtension> 
controllerServices,
+            final Collection<? extends VersionedConfigurableExtension> 
processors
+    ) {
+        final Map<String, VersionedConfigurableExtension> byVersionedId = 
HashMap.newHashMap(controllerServices.size() + processors.size());
+        addByVersionedId(byVersionedId, controllerServices);
+        addByVersionedId(byVersionedId, processors);
+        return byVersionedId;
+    }
+
+    private void addByVersionedId(
+            final Map<String, VersionedConfigurableExtension> byVersionedId,
+            final Collection<? extends VersionedConfigurableExtension> 
components
+    ) {
+        for (final VersionedConfigurableExtension component : components) {
+            byVersionedId.put(component.getIdentifier(), component);
+        }
+    }
+
+    private ComponentNode getSoleReferencer(final ControllerServiceNode 
localService) {
+        final Set<ComponentNode> referencers = 
localService.getReferences().getReferencingComponents();
+        if (referencers.size() != 1) {
+            return null;
+        }
+
+        return referencers.iterator().next();
+    }
+
+    private VersionedConfigurableExtension getProposedReferencer(
+            final ComponentNode referencer,
+            final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId
+    ) {
+        if (!(referencer instanceof 
org.apache.nifi.components.VersionedComponent versionedReferencer)) {
+            return null;
+        }
+
+        return versionedReferencer.getVersionedComponentId()
+            .map(proposedComponentsByVersionedId::get)
+            .orElse(null);
+    }
+
+    private ProposedControllerServiceMatch 
findMatchingProposedControllerService(
+            final ControllerServiceNode localService,
+            final ComponentNode referencer,
+            final VersionedConfigurableExtension proposedReferencer,
+            final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId,
+            final ProcessGroup group
+    ) {
+        for (final Map.Entry<PropertyDescriptor, String> propertyEntry : 
referencer.getRawPropertyValues().entrySet()) {
+            final PropertyDescriptor descriptor = propertyEntry.getKey();
+            final String propertyName = descriptor.getName();
+            if (descriptor.getControllerServiceDefinition() == null) {
+                continue;
+            }
+
+            if 
(!localService.getIdentifier().equals(propertyEntry.getValue())) {
+                continue;
+            }
+
+            final Map<String, String> proposedProperties = 
proposedReferencer.getProperties();
+            final String proposedServiceId = proposedProperties == null ? null 
: proposedProperties.get(propertyName);
+            if (proposedServiceId == null) {
+                LOG.debug("Leaving {} in {} unversioned because the proposed 
{} does not set the {} property",
+                    localService, group, proposedReferencer, propertyName);
+                continue;
+            }
+
+            // In versioned flow, the service identifier is the versioned 
component id of the service.
+            final VersionedConfigurableExtension proposedService = 
proposedComponentsByVersionedId.get(proposedServiceId);
+            if (proposedService == null || 
!proposedService.getType().equals(localService.getCanonicalClassName())) {
+                LOG.debug("Leaving {} in {} unversioned because proposed 
Controller Service {} is missing or has a different type than {}",
+                    localService, group, proposedServiceId, localService);
+                continue;
+            }
+
+            return new ProposedControllerServiceMatch(proposedServiceId, 
propertyName);
+        }
+
+        return null;
+    }
+
+    private record ProposedControllerServiceMatch(String versionedId, String 
propertyName) {
+    }

Review Comment:
   I recommend declaring this at the end of the class instead of in between 
methods



##########
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java:
##########
@@ -1169,9 +1172,228 @@ private void removeMissingRpg(final ProcessGroup group, 
final VersionedProcessGr
         removeMissingComponents(group, proposed, rpgsByVersionedId, 
VersionedProcessGroup::getRemoteProcessGroups, 
ProcessGroup::removeRemoteProcessGroup);
     }
 
+    /**
+     * Assigns a proposed versioned id to a Controller Service created by 
property migration.
+     * The service must have exactly one referencer.
+     * The proposed counterpart of that referencer must point at a Controller 
Service of the same type.
+     * If those do not hold, the service stays unversioned.
+     * A proposed id is assigned to at most one local service.
+     */
+    private void assignVersionedIdsToMigrationCreatedControllerServices(final 
ProcessGroup group, final VersionedProcessGroup proposed) {
+        final Collection<ControllerServiceNode> groupServices = 
group.getControllerServices(false);
+        if (groupServices.isEmpty()) {
+            return;
+        }
+
+        final List<ControllerServiceNode> migrationCreatedServices = 
getUnversionedMigrationCreatedServices(groupServices);
+        if (migrationCreatedServices.isEmpty()) {
+            return;
+        }
+
+        final Set<String> claimedVersionedIds = 
getClaimedVersionedIds(groupServices);
+
+        final Set<VersionedControllerService> proposedControllerServices = 
Objects.requireNonNullElse(proposed.getControllerServices(), Set.of());
+        final Set<VersionedProcessor> proposedProcessors = 
Objects.requireNonNullElse(proposed.getProcessors(), Set.of());
+        final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId = 
indexByVersionedId(proposedControllerServices, proposedProcessors);
+
+        for (final ControllerServiceNode localService : 
orderByReferencerChain(migrationCreatedServices)) {
+            assignMatchingVersionedId(group, localService, 
claimedVersionedIds, proposedComponentsByVersionedId);
+        }
+    }
+
+    private List<ControllerServiceNode> 
getUnversionedMigrationCreatedServices(final Collection<ControllerServiceNode> 
groupServices) {
+        final List<ControllerServiceNode> migrationCreatedServices = new 
ArrayList<>();
+        for (final ControllerServiceNode localService : groupServices) {
+            if (localService.getVersionedComponentId().isEmpty() && 
isMigrationCreated(localService)) {
+                migrationCreatedServices.add(localService);
+            }
+        }
+        return migrationCreatedServices;
+    }
+
+    private Set<String> getClaimedVersionedIds(final 
Collection<ControllerServiceNode> groupServices) {
+        final Set<String> claimedVersionedIds = 
HashSet.newHashSet(groupServices.size());
+        for (final ControllerServiceNode localService : groupServices) {
+            
localService.getVersionedComponentId().ifPresent(claimedVersionedIds::add);
+        }
+        return claimedVersionedIds;
+    }
+
+    private void assignMatchingVersionedId(
+            final ProcessGroup group,
+            final ControllerServiceNode localService,
+            final Set<String> claimedVersionedIds,
+            final Map<String, VersionedConfigurableExtension> 
proposedComponentsByVersionedId
+    ) {
+        final ComponentNode referencer = getSoleReferencer(localService);
+        if (referencer == null) {
+            LOG.debug("Leaving {} in {} unversioned because it is not 
referenced by exactly one component", localService, group);
+            return;
+        }
+
+        final VersionedConfigurableExtension proposedReferencer = 
getProposedReferencer(referencer, proposedComponentsByVersionedId);
+        if (proposedReferencer == null) {
+            LOG.debug("Leaving {} in {} unversioned because its referencer {} 
has no counterpart in the proposed flow", localService, group, referencer);
+            return;
+        }
+
+        final ProposedControllerServiceMatch match = 
findMatchingProposedControllerService(localService, referencer, 
proposedReferencer, proposedComponentsByVersionedId, group);
+        if (match == null) {
+            LOG.debug("Leaving {} in {} unversioned because no proposed 
Controller Service matches the referencing property of {}", localService, 
group, referencer);
+            return;
+        }
+
+        if (!claimedVersionedIds.add(match.versionedId())) {
+            LOG.debug("Leaving {} in {} unversioned because versioned id {} is 
already used by another Controller Service", localService, group, 
match.versionedId());
+            return;
+        }
+
+        localService.setVersionedComponentId(match.versionedId());
+        updatedVersionedComponentIds.add(match.versionedId());
+        LOG.info("Matched {} in {} to the Controller Service with versioned id 
{} that the proposed flow declares, based on the {} property of {}",
+            localService, group, match.versionedId(), match.propertyName(), 
referencer);
+    }
+
+    private List<ControllerServiceNode> orderByReferencerChain(final 
List<ControllerServiceNode> migrationCreatedServices) {
+        final BitSet visited = new BitSet(migrationCreatedServices.size());
+        final List<ControllerServiceNode> ordered = new 
ArrayList<>(migrationCreatedServices.size());
+
+        for (int i = 0; i < migrationCreatedServices.size(); i++) {
+            appendReferencerChain(i, migrationCreatedServices, visited, 
ordered);
+        }
+
+        return ordered;
+    }
+
+    private void appendReferencerChain(
+            final int index,
+            final List<ControllerServiceNode> migrationCreatedServices,
+            final BitSet visited,
+            final List<ControllerServiceNode> ordered
+    ) {
+        if (visited.get(index)) {
+            return;
+        }
+        visited.set(index);
+
+        final ControllerServiceNode localService = 
migrationCreatedServices.get(index);
+        final ComponentNode referencer = getSoleReferencer(localService);
+        if (referencer instanceof ControllerServiceNode referencingService && 
isMigrationCreated(referencingService)) {
+            final int referencerIndex = 
migrationCreatedServices.indexOf(referencingService);
+            if (referencerIndex >= 0) {
+                // Visit the creator first so we can assign the versioned id 
to the creator first.
+                appendReferencerChain(referencerIndex, 
migrationCreatedServices, visited, ordered);
+            }
+        }
+
+        ordered.add(localService);
+    }
+
+    private boolean isMigrationCreated(final ControllerServiceNode service) {
+        return 
StandardControllerServiceFactory.MIGRATION_CREATED_COMMENT.equals(service.getComments());
+    }
+
+    private Map<String, VersionedConfigurableExtension> indexByVersionedId(
+            final Collection<? extends VersionedConfigurableExtension> 
controllerServices,
+            final Collection<? extends VersionedConfigurableExtension> 
processors
+    ) {
+        final Map<String, VersionedConfigurableExtension> byVersionedId = 
HashMap.newHashMap(controllerServices.size() + processors.size());
+        addByVersionedId(byVersionedId, controllerServices);
+        addByVersionedId(byVersionedId, processors);
+        return byVersionedId;
+    }
+
+    private void addByVersionedId(
+            final Map<String, VersionedConfigurableExtension> byVersionedId,
+            final Collection<? extends VersionedConfigurableExtension> 
components
+    ) {
+        for (final VersionedConfigurableExtension component : components) {
+            byVersionedId.put(component.getIdentifier(), component);
+        }
+    }
+
+    private ComponentNode getSoleReferencer(final ControllerServiceNode 
localService) {
+        final Set<ComponentNode> referencers = 
localService.getReferences().getReferencingComponents();
+        if (referencers.size() != 1) {
+            return null;
+        }
+
+        return referencers.iterator().next();

Review Comment:
   This assumes there will never be `0` referencing components, which seems 
unsafe



-- 
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