markap14 commented on code in PR #7734:
URL: https://github.com/apache/nifi/pull/7734#discussion_r1506333554


##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {

Review Comment:
   We should avoid names like doXYZ(), xyzWork(), xyz0(), etc. and instead name 
according to what the method does. It is fine to simply name it 
`moveControllerService`



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);

Review Comment:
   We cannot wait for validation to complete here. This is called from a web 
thread, and must return ASAP. There should be no need to wait for validation 
here, though.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }
+                    if (controller != null) {
+                        if (!hasProcessGroup(controller.getProcessGroup(), 
newParent.getIdentifier())) {
+                            controller.removeReference(controllerService, 
prop.getKey());
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                }
+            }
+            if (!updatedProps.isEmpty())
+                controllerService.setProperties(updatedProps, true, 
Collections.emptySet());
+
+        } else {
+            // move to child process group
+            newParent = oldParentGroup.getProcessGroup(newProcessGroupID);
+            newParent.addControllerService(controllerService);
+
+            // unset any references for processors that are outside the new 
scope
+            for (ComponentNode node : referencedComponents) {
+                if (!hasProcessGroup(newParent, 
node.getProcessGroupIdentifier())) {
+                    Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = node.getProperties().entrySet();
+                    Map<String, String> updatedProps = new HashMap<>();
+                    for (Map.Entry<PropertyDescriptor, PropertyConfiguration> 
prop : properties) {
+                        final PropertyConfiguration value = prop.getValue();
+                        if (value != null && 
value.getRawValue().equals(controllerService.getIdentifier())) {
+                            controllerService.removeReference(node, 
prop.getKey());
+                            
node.getComponent().onPropertyModified(prop.getKey(), 
controllerService.getIdentifier(), null);
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                    node.setProperties(updatedProps, true, 
Collections.emptySet());
+                }
+            }
+        }
+
+        return controllerService;
+    }
+    private boolean hasProcessGroup(ProcessGroup root, String id){
+        if (root.getProcessGroup(id) != null || 
root.getIdentifier().equals(id)) {
+            return true;
+        }
+
+        for (ProcessGroup child : root.getProcessGroups()) {
+            if (hasProcessGroup(child, id)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void isReferencesDisabled(Set<ComponentNode> nodes) {

Review Comment:
   We should not have an `isXYZ` method that throws an Exception or that 
doesn't return a `boolean`. The typical naming convention here is 
`verifyReferencesDisabled`



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {

Review Comment:
   The `var` keyword should not be used.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }
+                    if (controller != null) {
+                        if (!hasProcessGroup(controller.getProcessGroup(), 
newParent.getIdentifier())) {
+                            controller.removeReference(controllerService, 
prop.getKey());
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                }
+            }
+            if (!updatedProps.isEmpty())
+                controllerService.setProperties(updatedProps, true, 
Collections.emptySet());
+
+        } else {
+            // move to child process group
+            newParent = oldParentGroup.getProcessGroup(newProcessGroupID);
+            newParent.addControllerService(controllerService);
+
+            // unset any references for processors that are outside the new 
scope
+            for (ComponentNode node : referencedComponents) {
+                if (!hasProcessGroup(newParent, 
node.getProcessGroupIdentifier())) {
+                    Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = node.getProperties().entrySet();
+                    Map<String, String> updatedProps = new HashMap<>();
+                    for (Map.Entry<PropertyDescriptor, PropertyConfiguration> 
prop : properties) {
+                        final PropertyConfiguration value = prop.getValue();
+                        if (value != null && 
value.getRawValue().equals(controllerService.getIdentifier())) {
+                            controllerService.removeReference(node, 
prop.getKey());
+                            
node.getComponent().onPropertyModified(prop.getKey(), 
controllerService.getIdentifier(), null);
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                    node.setProperties(updatedProps, true, 
Collections.emptySet());
+                }
+            }
+        }
+
+        return controllerService;
+    }
+    private boolean hasProcessGroup(ProcessGroup root, String id){

Review Comment:
   This method is unnecessary. There already exists a method on `ProcessGroup` 
named `findProcessGroup`. So you can simply use `root.findProcessGroup(id) != 
null`



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-service.js:
##########
@@ -2624,6 +2788,26 @@
             });
         },
 
+        /**
+         * Prompts the user before attempting to move the specified controller 
service.
+         *
+         * @param {jQuery} serviceTable
+         * @param {object} controllerServiceEntity
+         */
+        promptToMoveController: function (serviceTable, 
controllerServiceEntity) {
+            // prompt for move
+            nfDialog.showYesNoDialog({
+                headerText: 'Move Controller Service',
+                dialogContent: 'Move controller service \'' + 
nfCommon.escapeHtml(controllerServiceEntity.component.name) + '\' '
+                                + 'to \'' + 
nfCommon.escapeHtml($('#move-controller-service-scope').combo('getSelectedOption').text)
 + '\'?'
+                                + '<br /><br /><i class="invalid"></i> &nbsp'
+                                + 'Any reference to this controller service by 
processors outside the new scope will be removed.',
+                yesHandler: function () {
+                    moveToProcessGroup(serviceTable, controllerServiceEntity, 
$('#move-controller-service-scope').combo('getSelectedOption').value);
+                }
+            });
+        },

Review Comment:
   I don't think we need a prompt, either, after ensuring that references 
cannot become invalid. And because the service doesn't require being disabled, 
I think the option should always be shown.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java:
##########
@@ -680,6 +681,116 @@ public Response updateControllerService(
         );
     }
 
+    /**
+     * Moves the specified Controller Service to parent/child process groups.
+     *
+     * @param id The id of the controller service to update.
+     * @param requestControllerServiceEntity A controllerServiceEntity.
+     * @return A controllerServiceEntity.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/move")
+    @Operation(
+            summary = "Move Controller Service to the specified Process 
Group.",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = ControllerServiceEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Write - 
/controller-services/{uuid}"),
+                    @SecurityRequirement(name = "Write - Parent Process Group 
if scoped by Process Group - /process-groups/{uuid}"),
+                    @SecurityRequirement(name = "Write - Controller if scoped 
by Controller - /controller"),
+                    @SecurityRequirement(name = "Read - any referenced 
Controller Services - /controller-services/{uuid}")
+            })
+    @ApiResponses(
+            value = {
+                    @ApiResponse(responseCode = "400", description = "NiFi was 
unable to complete the request because it was invalid. The request should not 
be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client 
could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client 
is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The 
specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response moveControllerServices(
+            @Parameter(
+                    description = "The controller service id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The controller service entity",
+                    required = true
+            )
+            final ControllerServiceEntity requestControllerServiceEntity) {
+
+        if (requestControllerServiceEntity == null) {
+            throw new IllegalArgumentException("Controller service must be 
specified.");
+        }
+
+        if (requestControllerServiceEntity.getRevision() == null) {
+            throw new IllegalArgumentException("Revision must be specified.");
+        }
+
+        if (requestControllerServiceEntity.getParentGroupId() == null) {
+            throw new IllegalArgumentException("ParentGroupId must be 
specified.");
+        }
+
+        final ControllerServiceDTO requestControllerServiceDTO = 
serviceFacade.getControllerService(id, true).getComponent();
+        ControllerServiceState requestControllerServiceState = null;
+        try {
+            requestControllerServiceState = 
ControllerServiceState.valueOf(requestControllerServiceDTO.getState());
+        } catch (final IllegalArgumentException iae) {
+            // ignore
+        }
+
+        // ensure an action has been specified
+        if (requestControllerServiceState == null) {
+            throw new IllegalArgumentException("Must specify the updated 
state. To update the referencing Controller Services the "
+                    + "state should be DISABLED.");
+        }
+
+        // ensure the controller service state is not DISABLING
+        if 
(!ControllerServiceState.DISABLED.equals(requestControllerServiceState) ) {
+            throw new IllegalArgumentException("Cannot set the referencing 
services to ENABLING or DISABLING");
+        }

Review Comment:
   We should not have to care that the state is DISABLED. Given that all 
references must still be valid, moving the service to a new scope should not 
have any effect, and so it should be valid while the service is ENABLED.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }
+                    if (controller != null) {

Review Comment:
   No need for a null-check here, `controller` is guaranteed non-null by using 
`hasControllerService()` along with `continue`



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }
+                    if (controller != null) {
+                        if (!hasProcessGroup(controller.getProcessGroup(), 
newParent.getIdentifier())) {
+                            controller.removeReference(controllerService, 
prop.getKey());

Review Comment:
   We should never remove a reference to the Controller Service. Rather, we 
must ensure that the move is valid, and that all references will remain intact 
/ valid after the move. If not, the Controller Service cannot be moved.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }
+                    if (controller != null) {
+                        if (!hasProcessGroup(controller.getProcessGroup(), 
newParent.getIdentifier())) {
+                            controller.removeReference(controllerService, 
prop.getKey());
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                }
+            }
+            if (!updatedProps.isEmpty())
+                controllerService.setProperties(updatedProps, true, 
Collections.emptySet());

Review Comment:
   Should have `{}`:
   ```
               if (!updatedProps.isEmpty()) {
                   controllerService.setProperties(updatedProps, true, 
Collections.emptySet());
              }
   ```
   
   It is odd that the checkstyle didn't flag that.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }

Review Comment:
   We should not rely on Exceptions for flow control. Instead, use
   ```
   if (!controllerServiceDAO.hasControllerService(value.getRawValue()) {
     continue;
   }
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java:
##########
@@ -2939,6 +2939,115 @@ public ControllerServiceEntity 
updateControllerService(final Revision revision,
         return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
     }
 
+    @Override
+    public ControllerServiceEntity moveControllerService(final Revision 
revision, final ControllerServiceDTO controllerServiceDTO, final String 
newProcessGroupID) {
+        // get the component, ensure we have access to it, and perform the 
move request
+        final ControllerServiceNode controllerService = 
controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = 
updateComponent(revision,
+                controllerService,
+                () -> moveControllerServiceWork(controllerService, 
newProcessGroupID),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = 
dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = 
controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity 
referencingComponentsEntity = 
createControllerServiceReferencingComponentsEntity(ref);
+                    
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = 
dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = 
dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = 
dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = 
bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissions.getCanRead())).collect(Collectors.toList());
+        return 
entityFactory.createControllerServiceEntity(snapshot.getComponent(), 
dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, 
operatePermissions, bulletinEntities);
+    }
+
+    private ControllerServiceNode moveControllerServiceWork(final 
ControllerServiceNode controllerService, final String newProcessGroupID) {
+        final ProcessGroup oldParentGroup = 
controllerService.getProcessGroup();
+        Set<ComponentNode> referencedComponents = 
controllerService.getReferences().getReferencingComponents();
+        isReferencesDisabled(referencedComponents);
+        oldParentGroup.removeControllerService(controllerService);
+        ProcessGroup newParent;
+        if (!oldParentGroup.isRootGroup() && 
oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
+            // move to parent process group
+            newParent = oldParentGroup.getParent();
+            newParent.addControllerService(controllerService);
+
+            // unset any references the controller services has to other 
controller services that are now out of scope
+            Map<String, String> updatedProps = new HashMap<>();
+            Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = controllerService.getProperties().entrySet();
+            for (var prop : properties) {
+                var value = prop.getValue();
+                if (value !=null) {
+                    ControllerServiceNode controller;
+                    try {
+                        controller = 
controllerServiceDAO.getControllerService((value.getRawValue()));
+                    } catch (Exception e){
+                        continue;
+                    }
+                    if (controller != null) {
+                        if (!hasProcessGroup(controller.getProcessGroup(), 
newParent.getIdentifier())) {
+                            controller.removeReference(controllerService, 
prop.getKey());
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                }
+            }
+            if (!updatedProps.isEmpty())
+                controllerService.setProperties(updatedProps, true, 
Collections.emptySet());
+
+        } else {
+            // move to child process group
+            newParent = oldParentGroup.getProcessGroup(newProcessGroupID);
+            newParent.addControllerService(controllerService);
+
+            // unset any references for processors that are outside the new 
scope
+            for (ComponentNode node : referencedComponents) {
+                if (!hasProcessGroup(newParent, 
node.getProcessGroupIdentifier())) {
+                    Set<Map.Entry<PropertyDescriptor, PropertyConfiguration>> 
properties = node.getProperties().entrySet();
+                    Map<String, String> updatedProps = new HashMap<>();
+                    for (Map.Entry<PropertyDescriptor, PropertyConfiguration> 
prop : properties) {
+                        final PropertyConfiguration value = prop.getValue();
+                        if (value != null && 
value.getRawValue().equals(controllerService.getIdentifier())) {
+                            controllerService.removeReference(node, 
prop.getKey());
+                            
node.getComponent().onPropertyModified(prop.getKey(), 
controllerService.getIdentifier(), null);
+                            updatedProps.put(prop.getKey().getName(), null);
+                        }
+                    }
+                    node.setProperties(updatedProps, true, 
Collections.emptySet());
+                }
+            }

Review Comment:
   See above. We cannot remove references.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java:
##########
@@ -680,6 +681,116 @@ public Response updateControllerService(
         );
     }
 
+    /**
+     * Moves the specified Controller Service to parent/child process groups.
+     *
+     * @param id The id of the controller service to update.
+     * @param requestControllerServiceEntity A controllerServiceEntity.
+     * @return A controllerServiceEntity.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/move")
+    @Operation(
+            summary = "Move Controller Service to the specified Process 
Group.",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = ControllerServiceEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Write - 
/controller-services/{uuid}"),
+                    @SecurityRequirement(name = "Write - Parent Process Group 
if scoped by Process Group - /process-groups/{uuid}"),
+                    @SecurityRequirement(name = "Write - Controller if scoped 
by Controller - /controller"),
+                    @SecurityRequirement(name = "Read - any referenced 
Controller Services - /controller-services/{uuid}")
+            })
+    @ApiResponses(
+            value = {
+                    @ApiResponse(responseCode = "400", description = "NiFi was 
unable to complete the request because it was invalid. The request should not 
be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client 
could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client 
is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The 
specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response moveControllerServices(
+            @Parameter(
+                    description = "The controller service id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The controller service entity",
+                    required = true
+            )
+            final ControllerServiceEntity requestControllerServiceEntity) {
+
+        if (requestControllerServiceEntity == null) {
+            throw new IllegalArgumentException("Controller service must be 
specified.");
+        }
+
+        if (requestControllerServiceEntity.getRevision() == null) {
+            throw new IllegalArgumentException("Revision must be specified.");
+        }
+
+        if (requestControllerServiceEntity.getParentGroupId() == null) {
+            throw new IllegalArgumentException("ParentGroupId must be 
specified.");
+        }
+
+        final ControllerServiceDTO requestControllerServiceDTO = 
serviceFacade.getControllerService(id, true).getComponent();
+        ControllerServiceState requestControllerServiceState = null;
+        try {
+            requestControllerServiceState = 
ControllerServiceState.valueOf(requestControllerServiceDTO.getState());
+        } catch (final IllegalArgumentException iae) {
+            // ignore
+        }
+
+        // ensure an action has been specified
+        if (requestControllerServiceState == null) {
+            throw new IllegalArgumentException("Must specify the updated 
state. To update the referencing Controller Services the "
+                    + "state should be DISABLED.");
+        }

Review Comment:
   This error message may be confusing. If the request has a state of `foo`, 
this is going to indicate that the requestor must specify the updated state. 
Rather, it should indicate that the given state is invalid.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java:
##########
@@ -680,6 +681,116 @@ public Response updateControllerService(
         );
     }
 
+    /**
+     * Moves the specified Controller Service to parent/child process groups.
+     *
+     * @param id The id of the controller service to update.
+     * @param requestControllerServiceEntity A controllerServiceEntity.
+     * @return A controllerServiceEntity.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/move")
+    @Operation(
+            summary = "Move Controller Service to the specified Process 
Group.",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = ControllerServiceEntity.class))),
+            security = {
+                    @SecurityRequirement(name = "Write - 
/controller-services/{uuid}"),
+                    @SecurityRequirement(name = "Write - Parent Process Group 
if scoped by Process Group - /process-groups/{uuid}"),
+                    @SecurityRequirement(name = "Write - Controller if scoped 
by Controller - /controller"),
+                    @SecurityRequirement(name = "Read - any referenced 
Controller Services - /controller-services/{uuid}")
+            })
+    @ApiResponses(
+            value = {
+                    @ApiResponse(responseCode = "400", description = "NiFi was 
unable to complete the request because it was invalid. The request should not 
be retried without modification."),
+                    @ApiResponse(responseCode = "401", description = "Client 
could not be authenticated."),
+                    @ApiResponse(responseCode = "403", description = "Client 
is not authorized to make this request."),
+                    @ApiResponse(responseCode = "404", description = "The 
specified resource could not be found."),
+                    @ApiResponse(responseCode = "409", description = "The 
request was valid but NiFi was not in the appropriate state to process it.")
+            }
+    )
+    public Response moveControllerServices(
+            @Parameter(
+                    description = "The controller service id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @Parameter(
+                    description = "The controller service entity",
+                    required = true
+            )
+            final ControllerServiceEntity requestControllerServiceEntity) {
+
+        if (requestControllerServiceEntity == null) {
+            throw new IllegalArgumentException("Controller service must be 
specified.");
+        }
+
+        if (requestControllerServiceEntity.getRevision() == null) {
+            throw new IllegalArgumentException("Revision must be specified.");
+        }
+
+        if (requestControllerServiceEntity.getParentGroupId() == null) {
+            throw new IllegalArgumentException("ParentGroupId must be 
specified.");
+        }
+
+        final ControllerServiceDTO requestControllerServiceDTO = 
serviceFacade.getControllerService(id, true).getComponent();
+        ControllerServiceState requestControllerServiceState = null;
+        try {
+            requestControllerServiceState = 
ControllerServiceState.valueOf(requestControllerServiceDTO.getState());
+        } catch (final IllegalArgumentException iae) {
+            // ignore
+        }
+
+        // ensure an action has been specified
+        if (requestControllerServiceState == null) {
+            throw new IllegalArgumentException("Must specify the updated 
state. To update the referencing Controller Services the "
+                    + "state should be DISABLED.");
+        }
+
+        // ensure the controller service state is not DISABLING
+        if 
(!ControllerServiceState.DISABLED.equals(requestControllerServiceState) ) {
+            throw new IllegalArgumentException("Cannot set the referencing 
services to ENABLING or DISABLING");
+        }
+
+        final Revision requestRevision = 
getRevision(requestControllerServiceEntity, id);
+        return withWriteLock(
+                serviceFacade,
+                serviceFacade.getControllerService(id, true),
+                requestRevision,
+                lookup -> {
+                    // authorize the service
+                    final ComponentAuthorizable authorizable = 
lookup.getControllerService(requestControllerServiceDTO.getId());
+                    authorizable.getAuthorizable().authorize(authorizer, 
RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+
+                    // authorize the current and new process groups
+                    final ProcessGroupAuthorizable authorizableProcessGroupNew 
= lookup.getProcessGroup(requestControllerServiceEntity.getParentGroupId());
+                    
authorizableProcessGroupNew.getAuthorizable().authorize(authorizer, 
RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+
+                    final ProcessGroupAuthorizable authorizableProcessGroupOld 
= lookup.getProcessGroup(requestControllerServiceDTO.getParentGroupId());
+                    
authorizableProcessGroupOld.getAuthorizable().authorize(authorizer, 
RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+
+                    // authorize any referenced services
+                    
AuthorizeControllerServiceReference.authorizeControllerServiceReferences(requestControllerServiceDTO.getProperties(),
 authorizable, authorizer, lookup);
+                    
AuthorizeParameterReference.authorizeParameterReferences(requestControllerServiceDTO.getProperties(),
 authorizer, authorizable.getParameterContext(),
+                            NiFiUserUtils.getNiFiUser());
+
+                    // authorize referencing components
+                    
requestControllerServiceDTO.getReferencingComponents().forEach(e -> {
+                        final Authorizable controllerService = 
lookup.getControllerServiceReferencingComponent(requestControllerServiceDTO.getId(),
 e.getId());
+                        
OperationAuthorizable.authorizeOperation(controllerService, authorizer, 
NiFiUserUtils.getNiFiUser());
+                    });
+                },
+                null,

Review Comment:
   Verification step should go here, to ensure that any components that are 
referencing the Controller Service will still be able to reference the 
Controller Service once it has been moved. Otherwise, it should throw an 
IllegalStateException.



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