You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by bb...@apache.org on 2018/01/08 18:13:49 UTC

[01/50] nifi git commit: NIFI-4436: - Added the import dialog for importing a versioned flow into a new process group. - Added the change version dialog for upgrading/downgrading a versioned flow.

Repository: nifi
Updated Branches:
  refs/heads/master 8d4fe38bb -> b6117743d


http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp
index 4948001..524a824 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp
@@ -22,7 +22,7 @@
                 <div class="fa fa-question-circle" alt="Info" title="Specify the remote target NiFi URLs. Multiple URLs can be specified in comma-separated format. Different protocols cannot be mixed. If remote NiFi is a cluster, two or more node URLs are recommended for better connection establishment availability."></div>
              </div>
             <div class="setting-field">
-                <input id="new-remote-process-group-uris" type="text" placeholder="https://remotehost:8080/nifi"/>
+                <input id="new-remote-process-group-uris" type="text" placeholder="https://remotehost:8443/nifi"/>
             </div>
         </div>
         <div class="setting">

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
index 7fa90f7..56a5fd8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
@@ -25,9 +25,9 @@
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Location</div>
+            <div class="setting-name">URL</div>
             <div class="setting-field">
-                <input type="text" id="registry-location" class="setting-input"/>
+                <input type="text" id="registry-location" class="setting-input" placeholder="https://remotehost:8443"/>
             </div>
         </div>
         <div class="setting">

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
index dfed409..5e653d2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
@@ -20,34 +20,36 @@
         <div class="setting">
             <div class="setting-name">Registry</div>
             <div class="setting-field">
-                <div id="flow-version-registry-combo"></div>
-                <div id="flow-version-registry" class="hidden"></div>
+                <div id="save-flow-version-registry-combo" class="hidden"></div>
+                <div id="save-flow-version-registry" class="hidden"></div>
             </div>
         </div>
         <div class="setting">
             <div class="setting-name">Location</div>
             <div class="setting-field">
-                <div id="flow-version-bucket-combo"></div>
-                <div id="flow-version-bucket" class="hidden"></div>
+                <div id="save-flow-version-bucket-combo" class="hidden"></div>
+                <div id="save-flow-version-bucket" class="hidden"></div>
             </div>
         </div>
         <div class="setting">
             <div class="setting-name">Name</div>
             <div class="setting-field">
-                <span id="flow-version-process-group-id" class="hidden"></span>
-                <input type="text" id="flow-version-name" class="setting-input"/>
+                <span id="save-flow-version-process-group-id" class="hidden"></span>
+                <input type="text" id="save-flow-version-name-field" class="setting-input hidden"/>
+                <div id="save-flow-version-name" class="hidden"></div>
             </div>
         </div>
         <div class="setting">
             <div class="setting-name">Description</div>
             <div class="setting-field">
-                <textarea id="flow-version-description" class="setting-input"></textarea>
+                <textarea id="save-flow-version-description-field" class="setting-input hidden"></textarea>
+                <div id="save-flow-version-description" class="hidden"></div>
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Change Comments</div>
+            <div class="setting-name">Comments</div>
             <div class="setting-field">
-                <textarea id="flow-version-change-comments" class="setting-input"></textarea>
+                <textarea id="save-flow-version-change-comments" class="setting-input"></textarea>
             </div>
         </div>
     </div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index 1c44e71..f8f5177 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -213,14 +213,28 @@ div.progress-label {
 }
 
 /*
-    Flow Version
+    Save Flow Version
  */
 
-#flow-version-description, #flow-version-change-comments {
+#save-flow-version-description-field, #save-flow-version-change-comments {
     height: 85px;
 }
 
 /*
+    Import Flow Version
+ */
+
+#import-flow-version-table {
+    overflow: hidden;
+    position: absolute;
+    top: 202px;
+    left: 0px;
+    right: 0px;
+    bottom: 0px;
+    height: 225px;
+}
+
+/*
     Variable Registry
  */
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
index fcd4aba..ae9e228 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
@@ -24,9 +24,10 @@
                 'nf.Birdseye',
                 'nf.Graph',
                 'nf.CanvasUtils',
-                'nf.ErrorHandler'],
-            function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler) {
-                return (nf.ng.GroupComponent = factory($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler));
+                'nf.ErrorHandler',
+                'nf.Common'],
+            function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon) {
+                return (nf.ng.GroupComponent = factory($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.ng.GroupComponent =
@@ -35,16 +36,18 @@
                 require('nf.Birdseye'),
                 require('nf.Graph'),
                 require('nf.CanvasUtils'),
-                require('nf.ErrorHandler')));
+                require('nf.ErrorHandler'),
+                require('nf.Common')));
     } else {
         nf.ng.GroupComponent = factory(root.$,
             root.nf.Client,
             root.nf.Birdseye,
             root.nf.Graph,
             root.nf.CanvasUtils,
-            root.nf.ErrorHandler);
+            root.nf.ErrorHandler,
+            root.nf.Common);
     }
-}(this, function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler) {
+}(this, function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon) {
     'use strict';
 
     return function (serviceProvider) {
@@ -126,6 +129,7 @@
                         handler: {
                             close: function () {
                                 $('#new-process-group-name').val('');
+                                $('#new-process-group-dialog').removeData('pt');
                             }
                         }
                     });
@@ -145,10 +149,25 @@
                  * Show the modal.
                  */
                 show: function () {
+                    if (nfCommon.canVersionFlows()) {
+                        $('#import-process-group-link').show();
+                    } else {
+                        $('#import-process-group-link').hide();
+                    }
+
                     this.getElement().modal('show');
                 },
 
                 /**
+                 * Stores the pt.
+                 *
+                 * @param pt
+                 */
+                storePt: function (pt) {
+                    $('#new-process-group-dialog').data('pt', pt);
+                },
+
+                /**
                  * Hide the modal.
                  */
                 hide: function () {
@@ -255,6 +274,7 @@
                         }]);
 
                     // show the dialog
+                    groupComponent.modal.storePt(pt);
                     groupComponent.modal.show();
 
                     // set up the focus and key handlers

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
index ff09330..9dc20b1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
@@ -1254,14 +1254,14 @@
         },
 
         /**
-         * Reverts outstanding changes.
+         * Reverts local changes.
          */
-        revertFlowChanges: function (selection) {
+        revertLocalChanges: function (selection) {
             if (selection.empty()) {
-                nfFlowVersion.revertFlowChanges(nfCanvasUtils.getGroupId());
+                nfFlowVersion.revertLocalChanges(nfCanvasUtils.getGroupId());
             } else if (selection.size() === 1) {
                 var selectionData = selection.datum();
-                nfFlowVersion.revertFlowChanges(selectionData.id);
+                nfFlowVersion.revertLocalChanges(selectionData.id);
             }
         },
 
@@ -1269,18 +1269,25 @@
          * Changes the flow version.
          */
         changeFlowVersion: function (selection) {
-
+            if (selection.empty()) {
+                nfFlowVersion.showChangeFlowVersionDialog(nfCanvasUtils.getGroupId());
+            } else if (selection.size() === 1) {
+                var selectionData = selection.datum();
+                if (nfCanvasUtils.isProcessGroup(selection)) {
+                    nfFlowVersion.showChangeFlowVersionDialog(selectionData.id);
+                }
+            }
         },
 
         /**
          * Disconnects a Process Group from flow versioning.
          */
-        disconnectFlowVersioning: function (selection) {
+        stopVersionControl: function (selection) {
             if (selection.empty()) {
-                nfFlowVersion.disconnectFlowVersioning(nfCanvasUtils.getGroupId());
+                nfFlowVersion.stopVersionControl(nfCanvasUtils.getGroupId());
             } else if (selection.size() === 1) {
                 var selectionData = selection.datum();
-                nfFlowVersion.disconnectFlowVersioning(selectionData.id);
+                nfFlowVersion.stopVersionControl(selectionData.id);
             }
         },
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
index 6e17b27..536f87b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
@@ -337,7 +337,6 @@
                     nfCanvas.setManagedAuthorizer(configDetails.supportsManagedAuthorizer);
                     nfCanvas.setConfigurableAuthorizer(configDetails.supportsConfigurableAuthorizer);
                     nfCanvas.setConfigurableUsersAndGroups(configDetails.supportsConfigurableUsersAndGroups);
-                    nfCanvas.setSupportsFlowVersioning(configDetails.supportsFlowVersioning);
 
                     // init nfStorage
                     nfStorage.init();
@@ -356,7 +355,7 @@
                     nfQueueListing.init();
                     nfVariableRegistry.init();
                     nfComponentState.init();
-                    nfFlowVersion.init();
+                    nfFlowVersion.init(configDetails.timeOffset);
                     nfComponentVersion.init(nfSettings);
 
                     // initialize the component behaviors

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
index 9506fef..213b572 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
@@ -1820,13 +1820,6 @@
         },
 
         /**
-         * Returns whether this NiFi supports flow versioning.
-         */
-        supportsFlowVersioning: function () {
-            return nfCanvas.supportsFlowVersioning();
-        },
-
-        /**
          * Returns whether the authorizer is managed.
          */
         isManagedAuthorizer: function () {

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
index d0ad4ee..49ded8c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
@@ -85,7 +85,6 @@
     var permissions = null;
     var parentGroupId = null;
     var managedAuthorizer = false;
-    var supportsFlowVersioning = false;
     var configurableAuthorizer = false;
     var configurableUsersAndGroups = false;
     var svg = null;
@@ -910,23 +909,6 @@
         },
 
         /**
-         * Set whether this NiFi supports flow versioning.
-         *
-         * @param bool Whether this NiFi supports flow versioning
-         */
-        setSupportsFlowVersioning: function (bool) {
-            supportsFlowVersioning = bool;
-        },
-
-        /**
-         *
-         * @returns {boolean}
-         */
-        supportsFlowVersioning: function () {
-            return supportsFlowVersioning;
-        },
-
-        /**
          * Set whether the authorizer is configurable.
          *
          * @param bool The boolean value representing whether the authorizer is configurable.

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index f128086..8656035 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -375,11 +375,17 @@
      * @param selection
      */
     var supportsFlowVersioning = function (selection) {
-        if (nfCanvasUtils.supportsFlowVersioning() === false) {
+        if (nfCommon.canVersionFlows() === false) {
             return false;
         }
 
         if (selection.empty()) {
+            // prevent versioning of the root group
+            if (nfCanvasUtils.getParentGroupId() === null) {
+                return false;
+            }
+
+            // if not root group, ensure adequate permissions
             return nfCanvasUtils.canReadCurrentGroup() && nfCanvasUtils.canWriteCurrentGroup();
         }
 
@@ -632,13 +638,13 @@
         {id: 'variable-registry-menu-item', condition: hasVariables, menuItem: {clazz: 'fa', text: 'Variables', action: 'openVariableRegistry'}},
         {separator: true},
         {id: 'version-menu-item', groupMenuItem: {clazz: 'fa', text: 'Version'}, menuItems: [
-            {id: 'start-version-control-menu-item', condition: supportsStartFlowVersioning, menuItem: {clazz: 'fa fa-floppy-o', text: 'Start version control', action: 'saveFlowVersion'}},
+            {id: 'start-version-control-menu-item', condition: supportsStartFlowVersioning, menuItem: {clazz: 'fa fa-upload', text: 'Start version control', action: 'saveFlowVersion'}},
             {separator: true},
-            {id: 'commit-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-floppy-o', text: 'Commit local changes', action: 'saveFlowVersion'}},
-            {id: 'revert-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Revert local changes', action: 'revertFlowChanges'}},
+            {id: 'commit-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-upload', text: 'Commit local changes', action: 'saveFlowVersion'}},
+            {id: 'revert-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-undo', text: 'Revert local changes', action: 'revertLocalChanges'}},
             {id: 'change-version-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Change version', action: 'changeFlowVersion'}},
             {separator: true},
-            {id: 'stop-version-control-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Stop version control', action: 'disconnectFlowVersioning'}}
+            {id: 'stop-version-control-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Stop version control', action: 'stopVersionControl'}}
         ]},
         {separator: true},
         {id: 'enter-group-menu-item', condition: isProcessGroup, menuItem: {clazz: 'fa fa-sign-in', text: 'Enter group', action: 'enterGroup'}},

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 6315d30..1652624 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -30,9 +30,10 @@
                 'nf.Client',
                 'nf.CanvasUtils',
                 'nf.ProcessGroup',
-                'nf.Graph'],
-            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph) {
-                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph));
+                'nf.Graph',
+                'nf.Birdseye'],
+            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph, nfBirdseye) {
+                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph, nfBirdseye));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.FlowVerison =
@@ -44,7 +45,8 @@
                 require('nf.Client'),
                 require('nf.CanvasUtils'),
                 require('nf.ProcessGroup'),
-                require('nf.Graph')));
+                require('nf.Graph'),
+                require('nf.Birdseye')));
     } else {
         nf.FlowVersion = factory(root.$,
             root.nf.ng.Bridge,
@@ -54,35 +56,121 @@
             root.nf.Client,
             root.nf.CanvasUtils,
             root.nf.ProcessGroup,
-            root.nf.Graph);
+            root.nf.Graph,
+            root.nf.Birdseye);
     }
-}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph) {
+}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph, nfBirdseye) {
     'use strict';
 
+    var serverTimeOffset = null;
+
+    var gridOptions = {
+        forceFitColumns: true,
+        enableTextSelectionOnCells: true,
+        enableCellNavigation: true,
+        enableColumnReorder: false,
+        autoEdit: false,
+        multiSelect: false,
+        rowHeight: 24
+    };
+
+    /**
+     * Reset the save flow version dialog.
+     */
+    var resetSaveFlowVersionDialog = function () {
+        $('#save-flow-version-registry-combo').combo('destroy').hide();
+        $('#save-flow-version-bucket-combo').combo('destroy').hide();
+
+        $('#save-flow-version-registry').text('').hide();
+        $('#save-flow-version-bucket').text('').hide();
+
+        $('#save-flow-version-name').text('').hide();
+        $('#save-flow-version-description').text('').hide();
+
+        $('#save-flow-version-name-field').val('').hide();
+        $('#save-flow-version-description-field').val('').hide();
+        $('#save-flow-version-change-comments').val('');
+
+        $('#save-flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+    };
+
     /**
-     * Reset the dialog.
+     * Reset the import flow version dialog.
      */
-    var resetDialog = function () {
-        $('#flow-version-registry-combo').combo('destroy').hide();
-        $('#flow-version-bucket-combo').combo('destroy').hide();
+    var resetImportFlowVersionDialog = function () {
+        $('#import-flow-version-registry-combo').combo('destroy').hide();
+        $('#import-flow-version-bucket-combo').combo('destroy').hide();
+        $('#import-flow-version-name-combo').combo('destroy').hide();
 
-        $('#flow-version-registry').text('').hide();
-        $('#flow-version-bucket').text('').hide();
+        $('#import-flow-version-registry').text('').hide();
+        $('#import-flow-version-bucket').text('').hide();
+        $('#import-flow-version-name').text('').hide();
 
-        $('#flow-version-name').val('');
-        $('#flow-version-description').val('');
-        $('#flow-version-change-comments').val('');
+        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+        if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) {
+            var importFlowVersionData = importFlowVersionGrid.getData();
+            importFlowVersionData.setItems([]);
+        }
 
-        $('#flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+        $('#import-flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+    };
+
+    /**
+     * Loads the registries into the specified registry combo.
+     *
+     * @param dialog
+     * @param registryCombo
+     * @param bucketCombo
+     * @returns {deferred}
+     */
+    var loadRegistries = function (dialog, registryCombo, bucketCombo, selectBucket) {
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/flow/registries',
+            dataType: 'json'
+        }).done(function (registriesResponse) {
+            var registries = [];
+
+            if (nfCommon.isDefinedAndNotNull(registriesResponse.registries) && registriesResponse.registries.length > 0) {
+                registriesResponse.registries.sort(function (a, b) {
+                    return a.component.name > b.component.name;
+                });
+
+                $.each(registriesResponse.registries, function (_, registryEntity) {
+                    var registry = registryEntity.component;
+                    registries.push({
+                        text: registry.name,
+                        value: registry.id,
+                        description: nfCommon.escapeHtml(registry.description)
+                    });
+                });
+            } else {
+                registries.push({
+                    text: 'No available registries',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                });
+            }
+
+            // load the registries
+            registryCombo.combo({
+                options: registries,
+                select: function (selectedOption) {
+                    selectRegistry(dialog, selectedOption, bucketCombo, selectBucket)
+                }
+            });
+        }).fail(nfErrorHandler.handleAjaxError);
     };
 
     /**
      * Loads the buckets for the specified registryIdentifier for the current user.
      *
      * @param registryIdentifier
+     * @param bucketCombo
      * @returns {*}
      */
-    var loadBuckets = function (registryIdentifier) {
+    var loadBuckets = function (registryIdentifier, bucketCombo, selectBucket) {
         return $.ajax({
             type: 'GET',
             url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets',
@@ -113,23 +201,24 @@
             }
 
             // load the buckets
-            $('#flow-version-bucket-combo').combo('destroy').combo({
+            bucketCombo.combo('destroy').combo({
                 options: buckets,
                 select: selectBucket
             });
-        }).fail(function () {
-            $('#save-flow-version-dialog').modal('refreshButtons');
         }).fail(nfErrorHandler.handleAjaxError);
     };
 
     /**
      * Select handler for the registries combo.
      *
+     * @param dialog
      * @param selectedOption
+     * @param bucketCombo
+     * @param selectBucket
      */
-    var selectRegistry = function (selectedOption) {
-        if (selectedOption.disabled === true) {
-            $('#flow-version-bucket-combo').combo('destroy').combo({
+    var selectRegistry = function (dialog, selectedOption, bucketCombo, selectBucket) {
+        var showNoBucketsAvailable = function () {
+            bucketCombo.combo('destroy').combo({
                 options: [{
                     text: 'No available buckets',
                     value: null,
@@ -138,9 +227,15 @@
                 }]
             });
 
-            $('#save-flow-version-dialog').modal('refreshButtons');
+            dialog.modal('refreshButtons');
+        };
+
+        if (selectedOption.disabled === true) {
+            showNoBucketsAvailable();
         } else {
-            loadBuckets(selectedOption.value);
+            loadBuckets(selectedOption.value, bucketCombo, selectBucket).fail(function () {
+                showNoBucketsAvailable();
+            });
         }
     };
 
@@ -149,7 +244,7 @@
      *
      * @param selectedOption
      */
-    var selectBucket = function (selectedOption) {
+    var selectBucketSaveFlowVersion = function (selectedOption) {
         $('#save-flow-version-dialog').modal('refreshButtons');
     };
 
@@ -159,8 +254,8 @@
      * @returns {*}
      */
     var saveFlowVersion = function () {
-        var processGroupId = $('#flow-version-process-group-id').text();
-        var processGroupRevision = $('#flow-version-process-group-id').data('revision');
+        var processGroupId = $('#save-flow-version-process-group-id').text();
+        var processGroupRevision = $('#save-flow-version-process-group-id').data('revision');
 
         var saveFlowVersionRequest = {
             processGroupRevision: nfClient.getRevision({
@@ -170,27 +265,27 @@
             })
         };
 
-        var versionControlInformation = $('#flow-version-process-group-id').data('versionControlInformation');
+        var versionControlInformation = $('#save-flow-version-process-group-id').data('versionControlInformation');
         if (nfCommon.isDefinedAndNotNull(versionControlInformation)) {
             saveFlowVersionRequest['versionedFlow'] = {
                 registryId: versionControlInformation.registryId,
                 bucketId: versionControlInformation.bucketId,
                 flowId: versionControlInformation.flowId,
-                flowName: $('#flow-version-name').val(),
-                description: $('#flow-version-description').val(),
-                comments: $('#flow-version-change-comments').val()
+                flowName: $('#save-flow-version-name').text(),
+                description: $('#save-flow-version-description').text(),
+                comments: $('#save-flow-version-change-comments').val()
             }
         } else {
-            var selectedRegistry =  $('#flow-version-registry-combo').combo('getSelectedOption');
-            var selectedBucket =  $('#flow-version-bucket-combo').combo('getSelectedOption');
+            var selectedRegistry =  $('#save-flow-version-registry-combo').combo('getSelectedOption');
+            var selectedBucket =  $('#save-flow-version-bucket-combo').combo('getSelectedOption');
 
             saveFlowVersionRequest['versionedFlow'] = {
                 registryId: selectedRegistry.value,
                 bucketId: selectedBucket.value,
-                flowName: $('#flow-version-name').val(),
-                description: $('#flow-version-description').val(),
-                comments: $('#flow-version-change-comments').val()
-            }
+                flowName: $('#save-flow-version-name-field').val(),
+                description: $('#save-flow-version-description-field').val(),
+                comments: $('#save-flow-version-change-comments').val()
+            };
         }
 
         return $.ajax({
@@ -202,8 +297,555 @@
         }).fail(nfErrorHandler.handleAjaxError);
     };
 
+    /**
+     * Sorts the specified data using the specified sort details.
+     *
+     * @param {object} sortDetails
+     * @param {object} data
+     */
+    var sort = function (sortDetails, data) {
+        // defines a function for sorting
+        var comparer = function (a, b) {
+            var aString = nfCommon.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : '';
+            var bString = nfCommon.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : '';
+            return aString === bString ? 0 : aString > bString ? 1 : -1;
+        };
+
+        // perform the sort
+        data.sort(comparer, sortDetails.sortAsc);
+    };
+
+    var initImportFlowVersionTable = function () {
+        var importFlowVersionTable = $('#import-flow-version-table');
+
+        var valueFormatter = function (row, cell, value, columnDef, dataContext) {
+            return nfCommon.escapeHtml(value);
+        };
+
+        var timestampFormatter = function (row, cell, value, columnDef, dataContext) {
+            // get the current user time to properly convert the server time
+            var now = new Date();
+
+            // convert the user offset to millis
+            var userTimeOffset = now.getTimezoneOffset() * 60 * 1000;
+
+            // create the proper date by adjusting by the offsets
+            var date = new Date(dataContext.timestamp + userTimeOffset + serverTimeOffset);
+            return nfCommon.formatDateTime(date);
+        };
+
+        // define the column model for the controller services table
+        var importFlowVersionColumns = [
+            {
+                id: 'version',
+                name: 'Version',
+                field: 'version',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true,
+                width: 75,
+                maxWidth: 75
+            },
+            {
+                id: 'timestamp',
+                name: 'Created',
+                field: 'timestamp',
+                formatter: timestampFormatter,
+                sortable: true,
+                resizable: true,
+                width: 175,
+                maxWidth: 175
+            },
+            {
+                id: 'changeComments',
+                name: 'Comments',
+                field: 'comments',
+                sortable: true,
+                resizable: true,
+                formatter: valueFormatter
+            }
+        ];
+
+        // initialize the dataview
+        var importFlowVersionData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+
+        // initialize the sort
+        sort({
+            columnId: 'version',
+            sortAsc: false
+        }, importFlowVersionData);
+
+        // initialize the grid
+        var importFlowVersionGrid = new Slick.Grid(importFlowVersionTable, importFlowVersionData, importFlowVersionColumns, gridOptions);
+        importFlowVersionGrid.setSelectionModel(new Slick.RowSelectionModel());
+        importFlowVersionGrid.registerPlugin(new Slick.AutoTooltips());
+        importFlowVersionGrid.setSortColumn('version', false);
+        importFlowVersionGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.id,
+                sortAsc: args.sortAsc
+            }, importFlowVersionData);
+        });
+        importFlowVersionGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+            $('#import-flow-version-dialog').modal('refreshButtons');
+        });
+        importFlowVersionGrid.onDblClick.subscribe(function (e, args) {
+            changeFlowVersion().done(function () {
+                $('#import-flow-version-dialog').modal('hide');
+            });
+        });
+
+        // wire up the dataview to the grid
+        importFlowVersionData.onRowCountChanged.subscribe(function (e, args) {
+            importFlowVersionGrid.updateRowCount();
+            importFlowVersionGrid.render();
+        });
+        importFlowVersionData.onRowsChanged.subscribe(function (e, args) {
+            importFlowVersionGrid.invalidateRows(args.rows);
+            importFlowVersionGrid.render();
+        });
+        importFlowVersionData.syncGridSelection(importFlowVersionGrid, true);
+
+        // hold onto an instance of the grid
+        importFlowVersionTable.data('gridInstance', importFlowVersionGrid);
+    };
+
+    /**
+     * Shows the import flow version dialog.
+     */
+    var showImportFlowVersionDialog = function () {
+        var pt = $('#new-process-group-dialog').data('pt');
+
+        // update the registry and bucket visibility
+        var registryCombo = $('#import-flow-version-registry-combo').show();
+        var bucketCombo = $('#import-flow-version-bucket-combo').show();
+        $('#import-flow-version-name-combo').show();
+
+        loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, selectBucketImportVersion).done(function () {
+            // reposition the version table
+            $('#import-flow-version-table').css({
+                'top': '202px',
+                'height': '225px'
+            });
+
+            // show the import dialog
+            $('#import-flow-version-dialog').modal('setHeaderText', 'Import Version').modal('setButtonModel', [{
+                buttonText: 'Import',
+                color: {
+                    base: '#728E9B',
+                    hover: '#004849',
+                    text: '#ffffff'
+                },
+                disabled: disableImportOrChangeButton,
+                handler: {
+                    click: function () {
+                        importFlowVersion(pt).always(function (response) {
+                            // close the dialog
+                            $('#import-flow-version-dialog').modal('hide');
+                        });
+                    }
+                }
+            }, {
+                buttonText: 'Cancel',
+                color: {
+                    base: '#E3E8EB',
+                    hover: '#C7D2D7',
+                    text: '#004849'
+                },
+                handler: {
+                    click: function () {
+                        $(this).modal('hide');
+                    }
+                }
+            }]).modal('show');
+
+            // hide the new process group dialog
+            $('#new-process-group-dialog').modal('hide');
+        });
+    };
+
+    /**
+     * Loads the flow versions for the specified registry, bucket, and flow.
+     *
+     * @param registryIdentifier
+     * @param bucketIdentifier
+     * @param flowIdentifier
+     * @returns deferred
+     */
+    var loadFlowVersions = function (registryIdentifier, bucketIdentifier, flowIdentifier) {
+        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+        var importFlowVersionData = importFlowVersionGrid.getData();
+
+        // begin the update
+        importFlowVersionData.beginUpdate();
+
+        // remove the current versions
+        importFlowVersionGrid.setSelectedRows([]);
+        importFlowVersionGrid.resetActiveCell();
+        importFlowVersionData.setItems([]);
+
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets/' + encodeURIComponent(bucketIdentifier) + '/flows/' + encodeURIComponent(flowIdentifier) + '/versions',
+            dataType: 'json'
+        }).done(function (response) {
+            if (nfCommon.isDefinedAndNotNull(response.versionedFlowSnapshotMetadataSet) && response.versionedFlowSnapshotMetadataSet.length > 0) {
+                $.each(response.versionedFlowSnapshotMetadataSet, function (_, entity) {
+                    importFlowVersionData.addItem($.extend({
+                        id: entity.versionedFlowSnapshotMetadata.version
+                    }, entity.versionedFlowSnapshotMetadata));
+                });
+            } else {
+                nfDialog.showOkDialog({
+                    headerText: 'Flow Versions',
+                    dialogContent: 'This flow does not have any versions available.'
+                });
+            }
+        }).fail(nfErrorHandler.handleAjaxError).always(function () {
+            // end the update
+            importFlowVersionData.endUpdate();
+
+            // resort
+            importFlowVersionData.reSort();
+            importFlowVersionGrid.invalidate();
+        });
+    };
+
+    /**
+     * Loads the versioned flows from the specified registry and bucket.
+     *
+     * @param registryIdentifier
+     * @param bucketIdentifier
+     * @param selectFlow
+     * @returns deferred
+     */
+    var loadFlows = function (registryIdentifier, bucketIdentifier, selectFlow) {
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets/' + encodeURIComponent(bucketIdentifier) + '/flows',
+            dataType: 'json'
+        }).done(function (response) {
+            var versionedFlows = [];
+
+            if (nfCommon.isDefinedAndNotNull(response.versionedFlows) && response.versionedFlows.length > 0) {
+                response.versionedFlows.sort(function (a, b) {
+                    return a.versionedFlow.flowName > b.versionedFlow.flowName;
+                });
+
+                $.each(response.versionedFlows, function (_, versionedFlowEntity) {
+                    var versionedFlow = versionedFlowEntity.versionedFlow;
+                    versionedFlows.push({
+                        text: versionedFlow.flowName,
+                        value: versionedFlow.flowId,
+                        description: nfCommon.escapeHtml(versionedFlow.description)
+                    });
+                });
+            } else {
+                versionedFlows.push({
+                    text: 'No available flows',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                });
+            }
+
+            // load the buckets
+            $('#import-flow-version-name-combo').combo('destroy').combo({
+                options: versionedFlows,
+                select: function (selectedFlow) {
+                    if (nfCommon.isDefinedAndNotNull(selectedFlow.value)) {
+                        selectFlow(registryIdentifier, bucketIdentifier, selectedFlow.value)
+                    } else {
+                        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+                        var importFlowVersionData = importFlowVersionGrid.getData();
+
+                        // clear the current values
+                        importFlowVersionData.beginUpdate();
+                        importFlowVersionData.setItems([]);
+                        importFlowVersionData.endUpdate();
+                    }
+                }
+            });
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Handler when a versioned flow is selected.
+     *
+     * @param registryIdentifier
+     * @param bucketIdentifier
+     * @param flowIdentifier
+     */
+    var selectVersionedFlow = function (registryIdentifier, bucketIdentifier, flowIdentifier) {
+        loadFlowVersions(registryIdentifier, bucketIdentifier, flowIdentifier).done(function () {
+            $('#import-flow-version-dialog').modal('refreshButtons');
+        });
+    };
+
+    /**
+     * Handler when a bucket is selected.
+     *
+     * @param selectedBucket
+     */
+    var selectBucketImportVersion = function (selectedBucket) {
+        var selectedRegistry = $('#import-flow-version-registry-combo').combo('getSelectedOption');
+
+        // load the flows for the currently selected registry and bucket
+        loadFlows(selectedRegistry.value, selectedBucket.value, selectVersionedFlow);
+    };
+
+    /**
+     * Imports the selected flow version.
+     */
+    var importFlowVersion = function (pt) {
+        var selectedRegistry =  $('#import-flow-version-registry-combo').combo('getSelectedOption');
+        var selectedBucket =  $('#import-flow-version-bucket-combo').combo('getSelectedOption');
+        var selectedFlow =  $('#import-flow-version-name-combo').combo('getSelectedOption');
+
+        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+        var selectedVersionIndex = importFlowVersionGrid.getSelectedRows();
+        var selectedVersion = importFlowVersionGrid.getDataItem(selectedVersionIndex[0]);
+
+        var processGroupEntity = {
+            'revision': nfClient.getRevision({
+                'revision': {
+                    'version': 0
+                }
+            }),
+            'component': {
+                'name': selectedFlow.text, // TODO - name from versioned PG?
+                'position': {
+                    'x': pt.x,
+                    'y': pt.y
+                },
+                'versionControlInformation': {
+                    'registryId': selectedRegistry.value,
+                    'bucketId': selectedBucket.value,
+                    'flowId': selectedFlow.value,
+                    'version': selectedVersion.version
+                }
+            }
+        };
+
+        return $.ajax({
+            type: 'POST',
+            data: JSON.stringify(processGroupEntity),
+            url: '../nifi-api/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/process-groups',
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (response) {
+            // add the process group to the graph
+            nfGraph.add({
+                'processGroups': [response]
+            }, {
+                'selectAll': true
+            });
+
+            // update component visibility
+            nfGraph.updateVisibility();
+
+            // update the birdseye
+            nfBirdseye.refresh();
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Determines whether the import/change button is disabled.
+     *
+     * @returns {boolean}
+     */
+    var disableImportOrChangeButton = function () {
+        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+        if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) {
+            var selected = importFlowVersionGrid.getSelectedRows();
+            return selected.length !== 1;
+        } else {
+            return true;
+        }
+    };
+
+    /**
+     * Changes the flow version for the currently selected Process Group.
+     *
+     * @returns {deferred}
+     */
+    var changeFlowVersion = function () {
+        var changeTimer = null;
+
+        var processGroupId = $('#import-flow-version-process-group-id').text();
+        var processGroupRevision = $('#import-flow-version-process-group-id').data('revision');
+        var versionControlInformation = $('#import-flow-version-process-group-id').data('versionControlInformation');
+
+        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+        var selectedVersionIndex = importFlowVersionGrid.getSelectedRows();
+        var selectedVersion = importFlowVersionGrid.getDataItem(selectedVersionIndex[0]);
+
+        // TODO - introduce dialog to show current state with option to cancel once available
+
+        var submitChangeRequest = function () {
+            var changeVersionRequest = {
+                'processGroupRevision': nfClient.getRevision({
+                    'revision': {
+                        'version': processGroupRevision.version
+                    }
+                }),
+                'versionControlInformation': {
+                    'groupId': processGroupId,
+                    'registryId': versionControlInformation.registryId,
+                    'bucketId': versionControlInformation.bucketId,
+                    'flowId': versionControlInformation.flowId,
+                    'version': selectedVersion.version
+                }
+            };
+
+            return $.ajax({
+                type: 'POST',
+                data: JSON.stringify(changeVersionRequest),
+                url: '../nifi-api/versions/update-requests/process-groups/' + encodeURIComponent(processGroupId),
+                dataType: 'json',
+                contentType: 'application/json'
+            }).done(function (response) {
+                console.log(response);
+            }).fail(nfErrorHandler.handleAjaxError);
+        };
+
+        var pollChangeRequest = function (changeRequest) {
+            getChangeRequest(changeRequest).done(function (response) {
+                processChangeResponse(response);
+            })
+        };
+
+        var getChangeRequest = function (changeRequest) {
+            return $.ajax({
+                type: 'GET',
+                url: changeRequest.uri,
+                dataType: 'json'
+            }).fail(nfErrorHandler.handleAjaxError);
+        };
+
+        var deleteChangeRequest = function (changeRequest) {
+            var deleteXhr = $.ajax({
+                type: 'DELETE',
+                url: changeRequest.uri,
+                dataType: 'json'
+            }).fail(nfErrorHandler.handleAjaxError);
+
+            updateProcessGroup(processGroupId);
+
+            nfDialog.showOkDialog({
+                headerText: 'Change Version',
+                dialogContent: 'This Process Group version has changed.'
+            });
+
+            return deleteXhr;
+        };
+
+        var processChangeResponse = function (response) {
+            var changeRequest = response.request;
+
+            if (nfCommon.isDefinedAndNotNull(changeRequest.failureReason)) {
+                nfDialog.showOkDialog({
+                    headerText: 'Change Version',
+                    dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
+                });
+            }
+
+            if (changeRequest.complete === true) {
+                deleteChangeRequest(changeRequest);
+            } else {
+                changeTimer = setTimeout(function () {
+                    // clear the timer since we've been invoked
+                    changeTimer = null;
+
+                    // poll revert request
+                    pollChangeRequest(changeRequest);
+                }, 2000);
+            }
+        };
+
+        submitChangeRequest().done(function (response) {
+            processChangeResponse(response);
+        });
+
+    };
+
+    /**
+     * Gets the version control information for the specified process group id.
+     *
+     * @param processGroupId
+     * @return {deferred}
+     */
+    var getVersionControlInformation = function (processGroupId) {
+        return $.Deferred(function (deferred) {
+            if (processGroupId === nfCanvasUtils.getGroupId()) {
+                $.ajax({
+                    type: 'GET',
+                    url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                    dataType: 'json'
+                }).done(function (response) {
+                    deferred.resolve(response);
+                }).fail(function () {
+                    deferred.reject();
+                });
+            } else {
+                var processGroup = nfProcessGroup.get(processGroupId);
+                if (processGroup.permissions.canRead === true && processGroup.permissions.canWrite === true) {
+                    deferred.resolve({
+                        'processGroupRevision': processGroup.revision,
+                        'versionControlInformation': processGroup.component.versionControlInformation
+                    });
+                } else {
+                    deferred.reject();
+                }
+            }
+        }).promise();
+    };
+
+    /**
+     * Updates the specified process group with the specified version control information.
+     *
+     * @param processGroupId
+     * @param versionControlInformation
+     */
+    var updateVersionControlInformation = function (processGroupId, versionControlInformation) {
+        // refresh either selected PG or bread crumb to reflect connected/tracking status
+        if (nfCanvasUtils.getGroupId() === processGroupId) {
+            nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, versionControlInformation);
+            nfNgBridge.digest();
+        } else {
+            nfProcessGroup.reload(processGroupId);
+        }
+    };
+
+    /**
+     * Updates the specified process group following an operation that may change it's contents.
+     *
+     * @param processGroupId
+     */
+    var updateProcessGroup = function (processGroupId) {
+        if (nfCanvasUtils.getGroupId() === processGroupId) {
+            // if reverting current PG... reload/refresh this group/canvas
+
+            // TODO consider implementing this differently
+            $.ajax({
+                type: 'GET',
+                url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId),
+                dataType: 'json'
+            }).done(function (response) {
+                nfGraph.set(response.processGroupFlow.flow);
+            }).fail(nfErrorHandler.handleAjaxError);
+        } else {
+            // if reverting selected PG... reload selected PG to update counts, etc
+            nfProcessGroup.reload(processGroupId);
+        }
+    };
+
     return {
-        init: function () {
+        init: function (timeOffset) {
+            serverTimeOffset = timeOffset;
+
             // initialize the flow version dialog
             $('#save-flow-version-dialog').modal({
                 scrollableContentStyle: 'scrollable',
@@ -216,9 +858,9 @@
                         text: '#ffffff'
                     },
                     disabled: function () {
-                        if ($('#flow-version-registry-combo').is(':visible')) {
-                            var selectedRegistry =  $('#flow-version-registry-combo').combo('getSelectedOption');
-                            var selectedBucket =  $('#flow-version-bucket-combo').combo('getSelectedOption');
+                        if ($('#save-flow-version-registry-combo').is(':visible')) {
+                            var selectedRegistry =  $('#save-flow-version-registry-combo').combo('getSelectedOption');
+                            var selectedBucket =  $('#save-flow-version-bucket-combo').combo('getSelectedOption');
 
                             if (nfCommon.isDefinedAndNotNull(selectedRegistry) && nfCommon.isDefinedAndNotNull(selectedBucket)) {
                                 return selectedRegistry.disabled === true || selectedBucket.disabled === true;
@@ -231,16 +873,10 @@
                     },
                     handler: {
                         click: function () {
-                            var processGroupId = $('#flow-version-process-group-id').text();
+                            var processGroupId = $('#save-flow-version-process-group-id').text();
                             
                             saveFlowVersion().done(function (response) {
-                                // refresh either selected PG or bread crumb to reflect connected/tracking status
-                                if (nfCanvasUtils.getGroupId() === processGroupId) {
-                                    nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, response.versionControlInformation);
-                                    nfNgBridge.digest();
-                                } else {
-                                    nfProcessGroup.reload(processGroupId);
-                                }
+                                updateVersionControlInformation(processGroupId, response.versionControlInformation);
 
                                 // close the dialog
                                 $('#save-flow-version-dialog').modal('hide');
@@ -262,10 +898,28 @@
                 }],
                 handler: {
                     close: function () {
-                        resetDialog();
+                        resetSaveFlowVersionDialog();
                     }
                 }
             });
+
+            // initialize the import flow version dialog
+            $('#import-flow-version-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                handler: {
+                    close: function () {
+                        resetImportFlowVersionDialog();
+                    }
+                }
+            });
+
+            // handle the click for the process group import
+            $('#import-process-group-link').on('click', function() {
+                showImportFlowVersionDialog();
+            });
+
+            // initialize the import flow version table
+            initImportFlowVersionTable();
         },
 
         /**
@@ -274,88 +928,64 @@
          * @param processGroupId
          */
         showFlowVersionDialog: function (processGroupId) {
+            var focusName = true;
+
             return $.Deferred(function (deferred) {
-                $.ajax({
-                    type: 'GET',
-                    url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
-                    dataType: 'json'
-                }).done(function (response) {
+                getVersionControlInformation(processGroupId).done(function (groupVersionControlInformation) {
                     // record the revision
-                    $('#flow-version-process-group-id').data('revision', response.processGroupRevision).text(processGroupId);
+                    $('#save-flow-version-process-group-id').data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId);
 
-                    if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
-                        var versionControlInformation = response.versionControlInformation;
+                    if (nfCommon.isDefinedAndNotNull(groupVersionControlInformation.versionControlInformation)) {
+                        var versionControlInformation = groupVersionControlInformation.versionControlInformation;
 
                         // update the registry and bucket visibility
-                        $('#flow-version-registry').text(versionControlInformation.registryId).show();
-                        $('#flow-version-bucket').text(versionControlInformation.bucketId).show();
+                        $('#save-flow-version-registry').text(versionControlInformation.registryId).show();
+                        $('#save-flow-version-bucket').text(versionControlInformation.bucketId).show();
 
-                        $('#flow-version-name').val('');
-                        $('#flow-version-description').val('');
+                        $('#save-flow-version-name').text(versionControlInformation.flowName).show();
+                        $('#save-flow-version-description').text('Flow description goes here').show();
 
                         // record the versionControlInformation
-                        $('#flow-version-process-group-id').data('versionControlInformation', versionControlInformation)
+                        $('#save-flow-version-process-group-id').data('versionControlInformation', versionControlInformation);
 
+                        focusName = false;
                         deferred.resolve();
                     } else {
                         // update the registry and bucket visibility
-                        $('#flow-version-registry-combo').show();
-                        $('#flow-version-bucket-combo').show();
-
-                        $.ajax({
-                            type: 'GET',
-                            url: '../nifi-api/flow/registries',
-                            dataType: 'json'
-                        }).done(function (registriesResponse) {
-                            var registries = [];
-
-                            if (nfCommon.isDefinedAndNotNull(registriesResponse.registries) && registriesResponse.registries.length > 0) {
-                                registriesResponse.registries.sort(function (a, b) {
-                                    return a.component.name > b.component.name;
-                                });
-
-                                $.each(registriesResponse.registries, function (_, registryEntity) {
-                                    var registry = registryEntity.component;
-                                    registries.push({
-                                        text: registry.name,
-                                        value: registry.id,
-                                        description: nfCommon.escapeHtml(registry.description)
-                                    });
-                                });
-                            } else {
-                                registries.push({
-                                    text: 'No available registries',
-                                    value: null,
-                                    optionClass: 'unset',
-                                    disabled: true
-                                });
-                            }
+                        $('#save-flow-version-registry-combo').show();
+                        $('#save-flow-version-bucket-combo').show();
 
-                            // load the registries
-                            $('#flow-version-registry-combo').combo({
-                                options: registries,
-                                select: selectRegistry
-                            });
+                        $('#save-flow-version-name-field').show();
+                        $('#save-flow-version-description-field').show();
 
+                        loadRegistries($('#save-flow-version-dialog'), $('#save-flow-version-registry-combo'), $('#save-flow-version-bucket-combo'), selectBucketSaveFlowVersion).done(function () {
                             deferred.resolve();
                         }).fail(function () {
                             deferred.reject();
-                        }).fail(nfErrorHandler.handleAjaxError);
+                        });
                     }
                 }).fail(nfErrorHandler.handleAjaxError);
             }).done(function () {
                 $('#save-flow-version-dialog').modal('show');
+
+                if (focusName) {
+                    $('#save-flow-version-name-field').focus();
+                } else {
+                    $('#save-flow-version-change-comments').focus();
+                }
             }).fail(function () {
                 $('#save-flow-version-dialog').modal('refreshButtons');
             }).promise();
         },
 
         /**
-         * Reverts changes for the specified Process Group.
+         * Reverts local changes for the specified Process Group.
          *
          * @param processGroupId
          */
-        revertFlowChanges: function (processGroupId) {
+        revertLocalChanges: function (processGroupId) {
+            // TODO update to show user the ramifications of reverting for confirmation
+
             // prompt the user before reverting
             nfDialog.showYesNoDialog({
                 headerText: 'Revert Changes',
@@ -363,50 +993,88 @@
                 noText: 'Cancel',
                 yesText: 'Revert',
                 yesHandler: function () {
-                    $.ajax({
-                        type: 'GET',
-                        url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
-                        dataType: 'json'
-                    }).done(function (response) {
+                    getVersionControlInformation(processGroupId).done(function (response) {
                         if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
-                            var revertFlowVersionRequest = {
-                                processGroupRevision: nfClient.getRevision({
-                                    revision: {
-                                        version: response.processGroupRevision.version
-                                    }
-                                }),
-                                versionControlInformation: response.versionControlInformation
+                            var revertTimer = null;
+
+                            // TODO - introduce dialog to show current state once available
+
+                            var submitRevertRequest = function () {
+                                var revertFlowVersionRequest = {
+                                    'processGroupRevision': nfClient.getRevision({
+                                        'revision': {
+                                            'version': response.processGroupRevision.version
+                                        }
+                                    }),
+                                    'versionControlInformation': response.versionControlInformation
+                                };
+
+                                return $.ajax({
+                                    type: 'POST',
+                                    data: JSON.stringify(revertFlowVersionRequest),
+                                    url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
+                                    dataType: 'json',
+                                    contentType: 'application/json'
+                                }).fail(nfErrorHandler.handleAjaxError);
                             };
 
-                            $.ajax({
-                                type: 'POST',
-                                data: JSON.stringify(revertFlowVersionRequest),
-                                url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
-                                dataType: 'json',
-                                contentType: 'application/json'
-                            }).done(function (response) {
-                                // TODO update multi step to show user the ramifications of reverting for confirmation
-
-                                if (nfCanvasUtils.getGroupId() === processGroupId) {
-                                    // if reverting current PG... reload/refresh this group/canvas
-                                    // TODO consider implementing this differently
-                                    $.ajax({
-                                        type: 'GET',
-                                        url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId),
-                                        dataType: 'json'
-                                    }).done(function (response) {
-                                        nfGraph.set(response.processGroupFlow.flow);
-                                    }).fail(nfErrorHandler.handleAjaxError);
-                                } else {
-                                    // if reverting selected PG... reload selected PG to update counts, etc
-                                    nfProcessGroup.reload(processGroupId);
-                                }
+                            var pollRevertRequest = function (revertRequest) {
+                                getRevertRequest(revertRequest).done(function (response) {
+                                    processRevertResponse(response);
+                                })
+                            };
+
+                            var getRevertRequest = function (revertRequest) {
+                                return $.ajax({
+                                    type: 'GET',
+                                    url: revertRequest.uri,
+                                    dataType: 'json'
+                                }).fail(nfErrorHandler.handleAjaxError);
+                            };
+
+                            var deleteRevertRequest = function (revertRequest) {
+                                var deleteXhr = $.ajax({
+                                    type: 'DELETE',
+                                    url: revertRequest.uri,
+                                    dataType: 'json'
+                                }).fail(nfErrorHandler.handleAjaxError);
+
+                                updateProcessGroup(processGroupId);
 
                                 nfDialog.showOkDialog({
                                     headerText: 'Revert Changes',
                                     dialogContent: 'This Process Group has been reverted.'
                                 });
-                            }).fail(nfErrorHandler.handleAjaxError);
+
+                                return deleteXhr;
+                            };
+
+                            var processRevertResponse = function (response) {
+                                var revertRequest = response.request;
+
+                                if (nfCommon.isDefinedAndNotNull(revertRequest.failureReason)) {
+                                    nfDialog.showOkDialog({
+                                        headerText: 'Revert Changes',
+                                        dialogContent: nfCommon.escapeHtml(revertRequest.failureReason)
+                                    });
+                                }
+
+                                if (revertRequest.complete === true) {
+                                    deleteRevertRequest(revertRequest);
+                                } else {
+                                    revertTimer = setTimeout(function () {
+                                        // clear the timer since we've been invoked
+                                        revertTimer = null;
+
+                                        // poll revert request
+                                        pollRevertRequest(revertRequest);
+                                    }, 2000);
+                                }
+                            };
+
+                            submitRevertRequest().done(function (response) {
+                                processRevertResponse(response);
+                            });
                         } else {
                             nfDialog.showOkDialog({
                                 headerText: 'Revert Changes',
@@ -419,11 +1087,89 @@
         },
 
         /**
-         * Disconnects the specified Process Group from flow versioning.
+         * Shows the change flow version dialog.
          *
          * @param processGroupId
          */
-        disconnectFlowVersioning: function (processGroupId) {
+        showChangeFlowVersionDialog: function (processGroupId) {
+            return $.Deferred(function (deferred) {
+                getVersionControlInformation(processGroupId).done(function (groupVersionControlInformation) {
+                    if (nfCommon.isDefinedAndNotNull(groupVersionControlInformation.versionControlInformation)) {
+                        var versionControlInformation = groupVersionControlInformation.versionControlInformation;
+
+                        // update the registry and bucket visibility
+                        $('#import-flow-version-registry').text(versionControlInformation.registryId).show();
+                        $('#import-flow-version-bucket').text(versionControlInformation.bucketId).show();
+                        $('#import-flow-version-name').text(versionControlInformation.flowId).show();
+
+                        // record the versionControlInformation
+                        $('#import-flow-version-process-group-id').data('versionControlInformation', versionControlInformation).data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId);
+
+                        // load the flow versions
+                        loadFlowVersions(versionControlInformation.registryId, versionControlInformation.bucketId, versionControlInformation.flowId).done(function () {
+                            deferred.resolve();
+                        }).fail(function () {
+                            nfDialog.showOkDialog({
+                                headerText: 'Change Version',
+                                dialogContent: 'Unable to load available versions for this Process Group.'
+                            });
+
+                            deferred.reject();
+                        });
+                    } else {
+                        nfDialog.showOkDialog({
+                            headerText: 'Change Version',
+                            dialogContent: 'This Process Group is not currently under version control.'
+                        });
+
+                        deferred.reject();
+                    }
+                }).fail(nfErrorHandler.handleAjaxError);
+            }).done(function () {
+                // reposition the version table
+                $('#import-flow-version-table').css({
+                    'top': '150px',
+                    'height': '277px'
+                });
+
+                // show the dialog
+                $('#import-flow-version-dialog').modal('setHeaderText', 'Change Version').modal('setButtonModel', [{
+                    buttonText: 'Change',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    disabled: disableImportOrChangeButton,
+                    handler: {
+                        click: function () {
+                            changeFlowVersion().done(function () {
+                                $('#import-flow-version-dialog').modal('hide');
+                            });
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    color: {
+                        base: '#E3E8EB',
+                        hover: '#C7D2D7',
+                        text: '#004849'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }]).modal('show');
+            }).promise();
+        },
+
+        /**
+         * Stops version control for the specified Process Group.
+         *
+         * @param processGroupId
+         */
+        stopVersionControl: function (processGroupId) {
             // prompt the user before disconnecting
             nfDialog.showYesNoDialog({
                 headerText: 'Disconnect',
@@ -449,13 +1195,7 @@
                                 dataType: 'json',
                                 contentType: 'application/json'
                             }).done(function (response) {
-                                // refresh either selected PG or bread crumb to reflect disconnected status
-                                if (nfCanvasUtils.getGroupId() === processGroupId) {
-                                    nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, undefined);
-                                    nfNgBridge.digest();
-                                } else {
-                                    nfProcessGroup.reload(processGroupId);
-                                }
+                                updateVersionControlInformation(processGroupId, undefined);
 
                                 nfDialog.showOkDialog({
                                     headerText: 'Disconnect',

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
index 0e5a2d7..d3289c7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
@@ -82,8 +82,7 @@
             reportingTaskTypes: '../nifi-api/flow/reporting-task-types',
             createReportingTask: '../nifi-api/controller/reporting-tasks',
             reportingTasks: '../nifi-api/flow/reporting-tasks',
-            createRegistry: '../nifi-api/controller/registries',
-            registries: '../nifi-api/flow/registries'
+            registries: '../nifi-api/controller/registry-clients'
         }
     };
 
@@ -486,7 +485,7 @@
         // add the new registry
         var addRegistry = $.ajax({
             type: 'POST',
-            url: config.urls.createRegistry,
+            url: config.urls.registries,
             data: JSON.stringify(registryEntity),
             dataType: 'json',
             contentType: 'application/json'
@@ -877,7 +876,6 @@
         // initialize the registry configuration dialog
         $('#registry-configuration-dialog').modal({
             scrollableContentStyle: 'scrollable',
-            headerText: 'Add Registry',
             handler: {
                 close: function () {
                     $('#registry-id').text('');
@@ -1351,7 +1349,7 @@
         $('#registry-description').val(registryEntity.component.description);
 
         // show the dialog
-        $('#registry-configuration-dialog').modal('setButtonModel', [{
+        $('#registry-configuration-dialog').modal('setHeaderText', 'Edit Registry Client').modal('setButtonModel', [{
             buttonText: 'Update',
             color: {
                 base: '#728E9B',
@@ -1597,7 +1595,7 @@
                     name: 'Reporting Tasks',
                     tabContentId: 'reporting-tasks-tab-content'
                 }, {
-                    name: 'Registries',
+                    name: 'Registry Clients',
                     tabContentId: 'registries-tab-content'
                 }],
                 select: function () {
@@ -1625,9 +1623,9 @@
                                 } else if (tab === 'Reporting Tasks') {
                                     $('#settings-save').hide();
                                     return 'Create a new reporting task';
-                                } else if (tab === 'Registries') {
+                                } else if (tab === 'Registry Clients') {
                                     $('#settings-save').hide();
-                                    return 'Register a new registry';
+                                    return 'Register a new registry client';
                                 }
                             });
                         } else {
@@ -1637,7 +1635,7 @@
 
                         if (tab === 'Reporting Task Controller Services') {
                             $('#controller-cs-availability').show();
-                        } else if (tab === 'Reporting Tasks' || tab === 'Registries') {
+                        } else if (tab === 'Reporting Tasks' || tab === 'Registry Clients') {
                             $('#controller-cs-availability').hide();
                         }
 
@@ -1676,8 +1674,8 @@
 
                     // set the initial focus
                     $('#reporting-task-type-filter').focus();
-                } else if (selectedTab === 'Registries') {
-                    $('#registry-configuration-dialog').modal('setButtonModel', [{
+                } else if (selectedTab === 'Registry Clients') {
+                    $('#registry-configuration-dialog').modal('setHeaderText', 'Add Registry Client').modal('setButtonModel', [{
                         buttonText: 'Add',
                         color: {
                             base: '#728E9B',
@@ -1702,6 +1700,9 @@
                             }
                         }
                     }]).modal('show');
+
+                    // set the initial focus
+                    $('#registry-name').focus();
                 }
             });
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index 0d55b27..5619bc3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -557,6 +557,17 @@
         },
 
         /**
+         * Determines whether the current user can version flows.
+         */
+        canVersionFlows: function () {
+            if (nfCommon.isDefinedAndNotNull(nfCommon.currentUser)) {
+                return nfCommon.currentUser.canVersionFlows === true;
+            } else {
+                return false;
+            }
+        },
+
+        /**
          * Determines whether the current user can access provenance.
          *
          * @returns {boolean}


[38/50] nifi git commit: NIFI-4436: Bug fix to ensure that RPG's ports are not removed until after connections are established to the ports; ensure that if a registry's name is changed that it is updated immediately in VersionControlInformation objects

Posted by bb...@apache.org.
NIFI-4436: Bug fix to ensure that RPG's ports are not removed until after connections are established to the ports; ensure that if a registry's name is changed that it is updated immediately in VersionControlInformation objects

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/014c542f
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/014c542f
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/014c542f

Branch: refs/heads/master
Commit: 014c542f48b61c027152129ae7cd8c8535dd6a64
Parents: 49aad2c
Author: Mark Payne <ma...@hotmail.com>
Authored: Fri Dec 1 16:31:22 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:55 2018 -0500

----------------------------------------------------------------------
 .../apache/nifi/groups/RemoteProcessGroup.java  |  2 +
 .../apache/nifi/controller/FlowController.java  |  1 +
 .../controller/StandardFlowSynchronizer.java    |  2 +
 .../nifi/groups/StandardProcessGroup.java       | 12 ++-
 .../registry/flow/RestBasedFlowRegistry.java    | 42 +++++++---
 .../nifi/remote/StandardRemoteProcessGroup.java | 88 ++++++++++++--------
 .../org/apache/nifi/web/NiFiServiceFacade.java  | 11 +++
 .../nifi/web/StandardNiFiServiceFacade.java     | 25 ++++++
 .../nifi/web/api/ProcessGroupResource.java      | 12 +--
 .../apache/nifi/web/api/VersionsResource.java   | 15 +++-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  2 +-
 .../dao/impl/StandardRemoteProcessGroupDAO.java |  1 +
 12 files changed, 156 insertions(+), 57 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
index 7d92246..39be045 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
@@ -33,6 +33,8 @@ import java.util.concurrent.TimeUnit;
 
 public interface RemoteProcessGroup extends ComponentAuthorizable, Positionable, VersionedComponent {
 
+    void initialize();
+
     @Override
     String getIdentifier();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 2afa9dc..158aaa2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -1779,6 +1779,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
      */
     public void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto) throws ProcessorInstantiationException {
         instantiateSnippet(group, dto, true);
+        group.findAllRemoteProcessGroups().stream().forEach(RemoteProcessGroup::initialize);
     }
 
     private void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto, final boolean topLevel) throws ProcessorInstantiationException {

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index 28d9b79..9cbf323 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -360,6 +360,8 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
                         rootGroup = updateProcessGroup(controller, /* parent group */ null, rootGroupElement, encryptor, encodingVersion);
                     }
 
+                    rootGroup.findAllRemoteProcessGroups().forEach(RemoteProcessGroup::initialize);
+
                     // If there are any Templates that do not exist in the Proposed Flow that do exist in the 'existing flow', we need
                     // to ensure that we also add those to the appropriate Process Groups, so that we don't lose them.
                     final Document existingFlowConfiguration = parseFlowBytes(existingFlow);

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 4b186a9..fb3d3a6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -2964,6 +2964,13 @@ public final class StandardProcessGroup implements ProcessGroup {
             versionControlInformation.getStatus()) {
 
             @Override
+            public String getRegistryName() {
+                final String registryId = versionControlInformation.getRegistryIdentifier();
+                final FlowRegistry registry = flowController.getFlowRegistryClient().getFlowRegistry(registryId);
+                return registry == null ? registryId : registry.getName();
+            }
+
+            @Override
             public boolean isModified() {
                 boolean updated = false;
                 while (true) {
@@ -3220,7 +3227,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 updated = flowStatus.compareAndSet(status, updatedStatus);
             }
         } catch (final IOException | NiFiRegistryException e) {
-            final String message = String.format("Failed to synchronize Process Group with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry");
+            final String message = String.format("Failed to synchronize Process Group with Flow Registry : " + e.getMessage());
             setSyncFailedState(message);
 
             LOG.error("Failed to synchronize {} with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry", this, e);
@@ -3451,6 +3458,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             if (childGroup == null) {
                 final ProcessGroup added = addProcessGroup(group, proposedChildGroup, componentIdSeed, variablesToSkip);
+                added.findAllRemoteProcessGroups().stream().forEach(RemoteProcessGroup::initialize);
                 LOG.info("Added {} to {}", added, this);
             } else if (childCoordinates == null || updateDescendantVersionedGroups) {
                 updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName, updateDescendantVersionedGroups, variablesToSkip);
@@ -3759,7 +3767,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
         final Connectable destination = getConnectable(destinationGroup, proposed.getDestination());
         if (destination == null) {
-            throw new IllegalArgumentException("Connection has a destination with identifier " + proposed.getIdentifier()
+            throw new IllegalArgumentException("Connection has a destination with identifier " + proposed.getDestination().getId()
                 + " but no component could be found in the Process Group with a corresponding identifier");
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
index 1147b9e..21e5e0c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
@@ -115,40 +115,61 @@ public class RestBasedFlowRegistry implements FlowRegistry {
         return (user == null || user.isAnonymous()) ? null : user.getIdentity();
     }
 
+    private BucketClient getBucketClient(final NiFiUser user) {
+        final String identity = getIdentity(user);
+        final NiFiRegistryClient registryClient = getRegistryClient();
+        final BucketClient bucketClient = identity == null ? registryClient.getBucketClient() : registryClient.getBucketClient(identity);
+        return bucketClient;
+    }
+
+    private FlowSnapshotClient getFlowSnapshotClient(final NiFiUser user) {
+        final String identity = getIdentity(user);
+        final NiFiRegistryClient registryClient = getRegistryClient();
+        final FlowSnapshotClient snapshotClient = identity == null ? registryClient.getFlowSnapshotClient() : registryClient.getFlowSnapshotClient(identity);
+        return snapshotClient;
+    }
+
+    private FlowClient getFlowClient(final NiFiUser user) {
+        final String identity = getIdentity(user);
+        final NiFiRegistryClient registryClient = getRegistryClient();
+        final FlowClient flowClient = identity == null ? registryClient.getFlowClient() : registryClient.getFlowClient(identity);
+        return flowClient;
+    }
+
     @Override
     public Set<Bucket> getBuckets(final NiFiUser user) throws IOException, NiFiRegistryException {
-        final BucketClient bucketClient = getRegistryClient().getBucketClient(getIdentity(user));
+        final BucketClient bucketClient = getBucketClient(user);
         return new HashSet<>(bucketClient.getAll());
     }
 
     @Override
     public Bucket getBucket(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final BucketClient bucketClient = getRegistryClient().getBucketClient(getIdentity(user));
+        final BucketClient bucketClient = getBucketClient(user);
         return bucketClient.get(bucketId);
     }
 
 
     @Override
     public Set<VersionedFlow> getFlows(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
+        final FlowClient flowClient = getFlowClient(user);
         return new HashSet<>(flowClient.getByBucket(bucketId));
     }
 
     @Override
     public Set<VersionedFlowSnapshotMetadata> getFlowVersions(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
+        final FlowSnapshotClient snapshotClient = getFlowSnapshotClient(user);
         return new HashSet<>(snapshotClient.getSnapshotMetadata(bucketId, flowId));
     }
 
     @Override
     public VersionedFlow registerVersionedFlow(final VersionedFlow flow, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
+        final FlowClient flowClient = getFlowClient(user);
         return flowClient.create(flow);
     }
 
     @Override
     public VersionedFlow deleteVersionedFlow(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
+        final FlowClient flowClient = getFlowClient(user);
         return flowClient.delete(bucketId, flowId);
     }
 
@@ -156,7 +177,7 @@ public class RestBasedFlowRegistry implements FlowRegistry {
     public VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot,
         final String comments, final int expectedVersion, final NiFiUser user) throws IOException, NiFiRegistryException {
 
-        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
+        final FlowSnapshotClient snapshotClient = getFlowSnapshotClient(user);
         final VersionedFlowSnapshot versionedFlowSnapshot = new VersionedFlowSnapshot();
         versionedFlowSnapshot.setFlowContents(snapshot);
 
@@ -174,13 +195,14 @@ public class RestBasedFlowRegistry implements FlowRegistry {
 
     @Override
     public int getLatestVersion(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        return (int) getRegistryClient().getFlowClient(getIdentity(user)).get(bucketId, flowId).getVersionCount();
+        return (int) getFlowClient(user).get(bucketId, flowId).getVersionCount();
     }
 
     @Override
     public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version, final boolean fetchRemoteFlows, final NiFiUser user)
             throws IOException, NiFiRegistryException {
-        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
+
+        final FlowSnapshotClient snapshotClient = getFlowSnapshotClient(user);
         final VersionedFlowSnapshot flowSnapshot = snapshotClient.get(bucketId, flowId, version);
 
         if (fetchRemoteFlows) {
@@ -241,7 +263,7 @@ public class RestBasedFlowRegistry implements FlowRegistry {
 
     @Override
     public VersionedFlow getVersionedFlow(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
+        final FlowClient flowClient = getFlowClient(user);
         return flowClient.get(bucketId, flowId);
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index ef05a1b..5808500 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -16,6 +16,40 @@
  */
 package org.apache.nifi.remote;
 
+import static java.util.Objects.requireNonNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.core.Response;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.Resource;
 import org.apache.nifi.authorization.resource.Authorizable;
@@ -34,7 +68,6 @@ import org.apache.nifi.engine.FlowEngine;
 import org.apache.nifi.events.BulletinFactory;
 import org.apache.nifi.events.EventReporter;
 import org.apache.nifi.groups.ProcessGroup;
-import org.apache.nifi.groups.ProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor;
@@ -51,39 +84,6 @@ import org.apache.nifi.web.api.dto.PortDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.net.ssl.SSLContext;
-import javax.ws.rs.core.Response;
-import java.io.File;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import static java.util.Objects.requireNonNull;
-
 /**
  * Represents the Root Process Group of a remote NiFi Instance. Holds
  * information about that remote instance, as well as {@link IncomingPort}s and
@@ -104,6 +104,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
     private final EventReporter eventReporter;
     private final NiFiProperties nifiProperties;
     private final long remoteContentsCacheExpiration;
+    private volatile boolean initialized = false;
 
     private final AtomicReference<String> name = new AtomicReference<>();
     private final AtomicReference<Position> position = new AtomicReference<>(new Position(0D, 0D));
@@ -179,7 +180,16 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
 
         final Runnable checkAuthorizations = new InitializationTask();
         backgroundThreadExecutor = new FlowEngine(1, "Remote Process Group " + id, true);
-        backgroundThreadExecutor.scheduleWithFixedDelay(checkAuthorizations, 5L, 30L, TimeUnit.SECONDS);
+        backgroundThreadExecutor.scheduleWithFixedDelay(checkAuthorizations, 30L, 30L, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void initialize() {
+        if (initialized) {
+            return;
+        }
+
+        initialized = true;
         backgroundThreadExecutor.submit(() -> {
             try {
                 refreshFlowContents();
@@ -820,6 +830,10 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
 
     @Override
     public void refreshFlowContents() throws CommunicationsException {
+        if (!initialized) {
+            return;
+        }
+
         try {
             // perform the request
             final ControllerDTO dto;
@@ -1153,6 +1167,10 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
 
         @Override
         public void run() {
+            if (!initialized) {
+                return;
+            }
+
             try (final SiteToSiteRestApiClient apiClient = getSiteToSiteRestApiClient()) {
                 try {
                     final ControllerDTO dto = apiClient.getController(targetUris);

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 02df16b..be77d10 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -418,6 +418,17 @@ public interface NiFiServiceFacade {
     void verifyComponentTypes(VersionedProcessGroup versionedGroup);
 
     /**
+     * Verifies that the flow identified by the given Version Control Information can be imported into the Process Group
+     * with the given id
+     *
+     * @param versionControlInfo the information about the versioned flow
+     * @param groupId the ID of the Process Group where the flow should be instantiated
+     *
+     * @throws IllegalStateException if the flow cannot be imported into the specified group
+     */
+    void verifyImportProcessGroup(VersionControlInformationDTO versionControlInfo, String groupId);
+
+    /**
      * Creates a new Template based off the specified snippet.
      *
      * @param name name

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 4945296..4adb85b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -1863,6 +1863,31 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
+    public void verifyImportProcessGroup(final VersionControlInformationDTO versionControlInfo, final String groupId) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        verifyImportProcessGroup(versionControlInfo, group);
+    }
+
+    private void verifyImportProcessGroup(final VersionControlInformationDTO vciDto, final ProcessGroup group) {
+        if (group == null) {
+            return;
+        }
+
+        final VersionControlInformation vci = group.getVersionControlInformation();
+        if (vci != null) {
+            if (Objects.equals(vciDto.getRegistryId(), vci.getRegistryIdentifier())
+                && Objects.equals(vciDto.getBucketId(), vci.getBucketIdentifier())
+                && Objects.equals(vciDto.getFlowId(), vci.getFlowIdentifier())) {
+
+                throw new IllegalStateException("Cannot import the specified Versioned Flow into the Process Group because doing so would cause a recursive dataflow. "
+                    + "If Process Group A contains Process Group B, then Process Group B is not allowed to contain the flow identified by Process Group A.");
+            }
+        }
+
+        verifyImportProcessGroup(vciDto, group.getParent());
+    }
+
+    @Override
     public TemplateDTO createTemplate(final String name, final String description, final String snippetId, final String groupId, final Optional<String> idGenerationSeed) {
         // get the specified snippet
         final Snippet snippet = snippetDAO.getSnippet(snippetId);

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 7b753d6..a3bb5b2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -1641,6 +1641,10 @@ public class ProcessGroupResource extends ApplicationResource {
         // Step 6: Replicate the request or call serviceFacade.updateProcessGroup
 
         final VersionControlInformationDTO versionControlInfo = requestProcessGroupEntity.getComponent().getVersionControlInformation();
+        if (versionControlInfo != null) {
+            serviceFacade.verifyImportProcessGroup(versionControlInfo, groupId);
+        }
+
         if (versionControlInfo != null && requestProcessGroupEntity.getVersionedFlowSnapshot() == null) {
             // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
             // Step 2: Retrieve flow from Flow Registry
@@ -1685,12 +1689,8 @@ public class ProcessGroupResource extends ApplicationResource {
                         }
                     }
                 },
-                () -> {
-                    final VersionedFlowSnapshot versionedFlowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
-                    if (versionedFlowSnapshot != null) {
-                        serviceFacade.verifyComponentTypes(versionedFlowSnapshot.getFlowContents());
-                    }
-                },
+            () -> {
+            },
                 processGroupEntity -> {
                     final ProcessGroupDTO processGroup = processGroupEntity.getComponent();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 245713e..6dd641b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -424,15 +424,24 @@ public class VersionsResource extends ApplicationResource {
         if (versionedFlowDto == null) {
             throw new IllegalArgumentException("Version Control Information must be supplied.");
         }
-        if (versionedFlowDto.getBucketId() == null) {
+        if (StringUtils.isEmpty(versionedFlowDto.getBucketId())) {
             throw new IllegalArgumentException("The Bucket ID must be supplied.");
         }
-        if (versionedFlowDto.getFlowName() == null && versionedFlowDto.getFlowId() == null) {
+        if (StringUtils.isEmpty(versionedFlowDto.getFlowName()) && StringUtils.isEmpty(versionedFlowDto.getFlowId())) {
             throw new IllegalArgumentException("The Flow Name or Flow ID must be supplied.");
         }
-        if (versionedFlowDto.getRegistryId() == null) {
+        if (versionedFlowDto.getFlowName().length() > 1000) {
+            throw new IllegalArgumentException("The Flow Name cannot exceed 1,000 characters");
+        }
+        if (StringUtils.isEmpty(versionedFlowDto.getRegistryId())) {
             throw new IllegalArgumentException("The Registry ID must be supplied.");
         }
+        if (versionedFlowDto.getDescription() != null && versionedFlowDto.getDescription().length() > 65535) {
+            throw new IllegalArgumentException("Flow Description cannot exceed 65,535 characters");
+        }
+        if (versionedFlowDto.getComments() != null && versionedFlowDto.getComments().length() > 65535) {
+            throw new IllegalArgumentException("Comments cannot exceed 65,535 characters");
+        }
 
         // ensure we're not attempting to version the root group
         final ProcessGroupEntity root = serviceFacade.getProcessGroup(FlowController.ROOT_GROUP_ID_ALIAS);

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 8e5974a..7a1442d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -1792,7 +1792,7 @@ public final class DtoFactory {
         componentDto.setId(processorDto.getId());
         componentDto.setName(processorDto.getName());
         componentDto.setProcessGroupId(processorDto.getParentGroupId());
-        componentDto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        componentDto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
         componentDto.setState(processorDto.getState());
         componentDto.setValidationErrors(processorDto.getValidationErrors());
         component.setComponent(componentDto);

http://git-wip-us.apache.org/repos/asf/nifi/blob/014c542f/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
index c570dfc..941aae0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
@@ -85,6 +85,7 @@ public class StandardRemoteProcessGroupDAO extends ComponentDAO implements Remot
 
         // create the remote process group
         RemoteProcessGroup remoteProcessGroup = flowController.createRemoteProcessGroup(remoteProcessGroupDTO.getId(), targetUris);
+        remoteProcessGroup.initialize();
 
         // set other properties
         updateRemoteProcessGroup(remoteProcessGroup, remoteProcessGroupDTO);


[33/50] nifi git commit: NIFI-4436: Removed isCurrent, isModified from VersionControlInformation and associated DTO. Bug fixes & code refactoring

Posted by bb...@apache.org.
NIFI-4436: Removed isCurrent, isModified from VersionControlInformation and associated DTO. Bug fixes & code refactoring

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/fe8b30bf
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/fe8b30bf
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/fe8b30bf

Branch: refs/heads/master
Commit: fe8b30bf2608e6b321c54ad812e7a85e13acaa5f
Parents: db2cc9f
Author: Mark Payne <ma...@hotmail.com>
Authored: Tue Dec 5 16:18:16 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:55 2018 -0500

----------------------------------------------------------------------
 .../api/dto/VersionControlInformationDTO.java   |  22 ---
 .../manager/ProcessGroupEntityMerger.java       |   2 +-
 .../VersionControlInformationEntityMerger.java  |  53 +++++-
 .../flow/VersionControlInformation.java         |  11 --
 .../nifi/registry/flow/VersionedFlowState.java  |  21 ++-
 .../controller/StandardFlowSynchronizer.java    |   5 +-
 .../nifi/groups/StandardProcessGroup.java       | 165 +++++++++----------
 .../groups/StandardVersionedFlowStatus.java     |  11 +-
 .../nifi/groups/VersionControlFields.java       |  61 +++++++
 .../flow/StandardVersionControlInformation.java |  51 ++----
 .../nifi/remote/StandardRemoteProcessGroup.java |   5 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  14 --
 .../nifi/web/StandardNiFiServiceFacade.java     | 109 +++++++++---
 .../nifi/web/api/ProcessGroupResource.java      |  77 +++++----
 .../apache/nifi/web/api/VersionsResource.java   |  33 ++--
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  38 +----
 .../web/dao/impl/StandardProcessGroupDAO.java   |   8 +-
 .../dao/impl/StandardRemoteProcessGroupDAO.java |   5 +-
 .../src/main/resources/nifi-web-api-context.xml |   2 -
 19 files changed, 384 insertions(+), 309 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
index 944b10a..21864b0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
@@ -32,8 +32,6 @@ public class VersionControlInformationDTO {
     private String flowName;
     private String flowDescription;
     private Integer version;
-    private Boolean modified;
-    private Boolean current;
     private String state;
     private String stateExplanation;
 
@@ -118,26 +116,6 @@ public class VersionControlInformationDTO {
         this.version = version;
     }
 
-    @ApiModelProperty(readOnly=true,
-        value = "Whether or not the flow has been modified since it was last synced to the Flow Registry. The value will be null if this information is not yet known.")
-    public Boolean getModified() {
-        return modified;
-    }
-
-    public void setModified(Boolean modified) {
-        this.modified = modified;
-    }
-
-    @ApiModelProperty(readOnly=true,
-        value = "Whether or not this is the most recent version of the flow in the Flow Registry. The value will be null if this information is not yet known.")
-    public Boolean getCurrent() {
-        return current;
-    }
-
-    public void setCurrent(Boolean current) {
-        this.current = current;
-    }
-
     @ApiModelProperty(readOnly = true,
         value = "The current state of the Process Group, as it relates to the Versioned Flow",
         allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
index 457e75b..d2eb749 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
@@ -55,7 +55,7 @@ public class ProcessGroupEntityMerger implements ComponentEntityMerger<ProcessGr
         if (targetVersionControl == null) {
             targetGroupDto.setVersionControlInformation(toMergeGroupDto.getVersionControlInformation());
         } else if (toMergeVersionControl != null) {
-            targetVersionControl.setCurrent(Boolean.TRUE.equals(targetVersionControl.getCurrent()) && Boolean.TRUE.equals(toMergeVersionControl.getCurrent()));
+            VersionControlInformationEntityMerger.updateFlowState(targetVersionControl, toMergeVersionControl);
         }
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
index 8d102df..f8ab4c0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
@@ -20,6 +20,7 @@ package org.apache.nifi.cluster.manager;
 import java.util.Map;
 
 import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 
@@ -37,12 +38,54 @@ public class VersionControlInformationEntityMerger {
             .forEach(entity -> {
                 final VersionControlInformationDTO dto = entity.getVersionControlInformation();
 
-                // We consider the flow to be current only if ALL nodes indicate that it is current
-                clientDto.setCurrent(Boolean.TRUE.equals(clientDto.getCurrent()) && Boolean.TRUE.equals(dto.getCurrent()));
-
-                // We consider the flow to be modified if ANY node indicates that it is modified
-                clientDto.setModified(Boolean.TRUE.equals(clientDto.getModified()) || Boolean.TRUE.equals(dto.getModified()));
+                updateFlowState(clientDto, dto);
             });
     }
 
+
+    private static boolean isCurrent(final VersionedFlowState state) {
+        return state == VersionedFlowState.UP_TO_DATE || state == VersionedFlowState.LOCALLY_MODIFIED;
+    }
+
+    private static boolean isModified(final VersionedFlowState state) {
+        return state == VersionedFlowState.LOCALLY_MODIFIED || state == VersionedFlowState.LOCALLY_MODIFIED_AND_STALE;
+    }
+
+    public static void updateFlowState(final VersionControlInformationDTO clientDto, final VersionControlInformationDTO dto) {
+        final VersionedFlowState clientState = VersionedFlowState.valueOf(clientDto.getState());
+        if (clientState == VersionedFlowState.SYNC_FAILURE) {
+            return;
+        }
+
+        final VersionedFlowState dtoState = VersionedFlowState.valueOf(dto.getState());
+        if (dtoState == VersionedFlowState.SYNC_FAILURE) {
+            clientDto.setState(dto.getState());
+            clientDto.setStateExplanation(dto.getStateExplanation());
+            return;
+        }
+
+        final boolean clientCurrent = isCurrent(clientState);
+        final boolean clientModified = isModified(clientState);
+
+        final boolean dtoCurrent = isCurrent(dtoState);
+        final boolean dtoModified = isModified(dtoState);
+
+        final boolean current = clientCurrent && dtoCurrent;
+        final boolean stale = !current;
+        final boolean modified = clientModified && dtoModified;
+
+        final VersionedFlowState flowState;
+        if (modified && stale) {
+            flowState = VersionedFlowState.LOCALLY_MODIFIED_AND_STALE;
+        } else if (modified) {
+            flowState = VersionedFlowState.LOCALLY_MODIFIED;
+        } else if (stale) {
+            flowState = VersionedFlowState.STALE;
+        } else {
+            flowState = VersionedFlowState.UP_TO_DATE;
+        }
+
+        clientDto.setState(flowState.name());
+        clientDto.setStateExplanation(flowState.getDescription());
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
index 1f65a19..bb4e0d0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
@@ -66,17 +66,6 @@ public interface VersionControlInformation {
     int getVersion();
 
     /**
-     * @return <code>true</code> if the flow has been modified since the last time that it was updated from the Flow Registry or saved
-     *         to the Flow Registry; <code>false</code> if the flow is in sync with the Flow Registry.
-     */
-    boolean isModified();
-
-    /**
-     * @return <code>true</code> if this version of the flow is the most recent version of the flow available in the Flow Registry, <code>false</code> otherwise.
-     */
-    boolean isCurrent();
-
-    /**
      * @return the current status of the Process Group as it relates to the associated Versioned Flow.
      */
     VersionedFlowStatus getStatus();

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
index d20a13f..35b436d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
@@ -22,31 +22,42 @@ public enum VersionedFlowState {
     /**
      * We are unable to communicate with the Flow Registry in order to determine the appropriate state
      */
-    SYNC_FAILURE,
+    SYNC_FAILURE("Failed to communicate with Flow Registry"),
 
     /**
      * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
      * is on the latest version of the Versioned Flow, but is different than the Versioned Flow that is
      * stored in the Flow Registry.
      */
-    LOCALLY_MODIFIED,
+    LOCALLY_MODIFIED("Local changes have been made"),
 
     /**
      * This Process Group has not been modified since it was last synchronized with the Flow Registry, but
      * the Flow Registry has a newer version of the flow than what is contained in this Process Group.
      */
-    STALE,
+    STALE("A newer version of this flow is available"),
 
     /**
      * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
      * has been modified since it was last synchronized with the Flow Registry, and the Flow Registry has
      * a newer version of the flow than what is contained in this Process Group.
      */
-    LOCALLY_MODIFIED_AND_STALE,
+    LOCALLY_MODIFIED_AND_STALE("Local changes have been made and a newer version of this flow is available"),
 
     /**
      * This Process Group and all child/descendant Process Groups are on the latest version of the flow in
      * the Flow Registry and have no local modifications.
      */
-    UP_TO_DATE;
+    UP_TO_DATE("Flow version is current");
+
+
+    private final String description;
+
+    private VersionedFlowState(final String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index 9cbf323..9bb3d2f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -88,6 +88,7 @@ import org.apache.nifi.processor.SimpleProcessLogger;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
@@ -1116,10 +1117,10 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
             final FlowRegistry flowRegistry = controller.getFlowRegistryClient().getFlowRegistry(versionControlInfoDto.getRegistryId());
             final String registryName = flowRegistry == null ? versionControlInfoDto.getRegistryId() : flowRegistry.getName();
 
+            versionControlInfoDto.setState(VersionedFlowState.SYNC_FAILURE.name());
+            versionControlInfoDto.setStateExplanation("Process Group has not yet been synchronized with the Flow Registry");
             final StandardVersionControlInformation versionControlInformation = StandardVersionControlInformation.Builder.fromDto(versionControlInfoDto)
                 .registryName(registryName)
-                .modified(false)
-                .current(true)
                 .build();
 
             // pass empty map for the version control mapping because the VersionedComponentId has already been set on the components

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index fb3d3a6..7d184df 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -169,8 +169,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     private final Map<String, Template> templates = new HashMap<>();
     private final StringEncryptor encryptor;
     private final MutableVariableRegistry variableRegistry;
-    private final AtomicReference<StandardVersionedFlowStatus> flowStatus = new AtomicReference<>(
-        new StandardVersionedFlowStatus(null, "Not yet synchronized with Flow Registry", null));
+    private final VersionControlFields versionControlFields = new VersionControlFields();
 
     private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
     private final Lock readLock = rwLock.readLock();
@@ -339,14 +338,22 @@ public final class StandardProcessGroup implements ProcessGroup {
                 // update the vci counts for this child group
                 final VersionControlInformation vci = childGroup.getVersionControlInformation();
                 if (vci != null) {
-                    if (vci.isModified() && !vci.isCurrent()) {
-                        locallyModifiedAndStale += 1;
-                    } else if (!vci.isCurrent()) {
-                        stale += 1;
-                    } else if (vci.isModified()) {
-                        locallyModified += 1;
-                    } else {
-                        upToDate += 1;
+                    switch (vci.getStatus().getState()) {
+                        case LOCALLY_MODIFIED:
+                            locallyModified++;
+                            break;
+                        case LOCALLY_MODIFIED_AND_STALE:
+                            locallyModifiedAndStale++;
+                            break;
+                        case STALE:
+                            stale++;
+                            break;
+                        case SYNC_FAILURE:
+                            syncFailure++;
+                            break;
+                        case UP_TO_DATE:
+                            upToDate++;
+                            break;
                     }
                 }
 
@@ -2938,17 +2945,9 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
         }
 
-        clearFlowDifferences();
+        versionControlFields.setFlowDifferences(null);
     }
 
-    private void clearFlowDifferences() {
-        boolean updated = false;
-        while (!updated) {
-            final StandardVersionedFlowStatus status = flowStatus.get();
-            final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(status.getState(), status.getStateExplanation(), null);
-            updated = flowStatus.compareAndSet(status, updatedStatus);
-        }
-    }
 
     @Override
     public void setVersionControlInformation(final VersionControlInformation versionControlInformation, final Map<String, String> versionedComponentIds) {
@@ -2959,8 +2958,6 @@ public final class StandardProcessGroup implements ProcessGroup {
             versionControlInformation.getFlowIdentifier(),
             versionControlInformation.getVersion(),
             stripContentsFromRemoteDescendantGroups(versionControlInformation.getFlowSnapshot(), true),
-            versionControlInformation.isModified(),
-            versionControlInformation.isCurrent(),
             versionControlInformation.getStatus()) {
 
             @Override
@@ -2970,60 +2967,50 @@ public final class StandardProcessGroup implements ProcessGroup {
                 return registry == null ? registryId : registry.getName();
             }
 
-            @Override
-            public boolean isModified() {
-                boolean updated = false;
-                while (true) {
-                    final StandardVersionedFlowStatus status = flowStatus.get();
-                    Set<FlowDifference> differences = status.getCurrentDifferences();
+            private boolean isModified() {
+                Set<FlowDifference> differences = versionControlFields.getFlowDifferences();
+                if (differences == null) {
+                    differences = getModifications();
                     if (differences == null) {
-                        differences = getModifications();
-                        if (differences == null) {
-                            return false;
-                        }
-
-                        final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(status.getState(), status.getStateExplanation(), differences);
-                        updated = flowStatus.compareAndSet(status, updatedStatus);
-
-                        if (updated) {
-                            return !differences.isEmpty();
-                        }
-
-                        continue;
+                        return false;
                     }
 
-                    return !differences.isEmpty();
+                    versionControlFields.setFlowDifferences(differences);
                 }
+
+                return !differences.isEmpty();
             }
 
             @Override
             public VersionedFlowStatus getStatus() {
                 // If current state is a sync failure, then
-                final StandardVersionedFlowStatus status = flowStatus.get();
-                final VersionedFlowState state = status.getState();
-                if (state == VersionedFlowState.SYNC_FAILURE) {
-                    return status;
+                final String syncFailureExplanation = versionControlFields.getSyncFailureExplanation();
+                if (syncFailureExplanation != null) {
+                    return new StandardVersionedFlowStatus(VersionedFlowState.SYNC_FAILURE, syncFailureExplanation);
                 }
 
                 final boolean modified = isModified();
                 if (!modified) {
                     final VersionControlInformation vci = StandardProcessGroup.this.versionControlInfo.get();
                     if (vci.getFlowSnapshot() == null) {
-                        return new StandardVersionedFlowStatus(VersionedFlowState.SYNC_FAILURE, "Process Group has not yet been synchronized with Flow Registry", null);
+                        return new StandardVersionedFlowStatus(VersionedFlowState.SYNC_FAILURE, "Process Group has not yet been synchronized with Flow Registry");
                     }
                 }
 
-                final boolean stale = !isCurrent();
+                final boolean stale = versionControlFields.isStale();
 
+                final VersionedFlowState flowState;
                 if (modified && stale) {
-                    return new StandardVersionedFlowStatus(VersionedFlowState.LOCALLY_MODIFIED_AND_STALE, "Local changes have been made and a newer version of this flow is available", null);
+                    flowState = VersionedFlowState.LOCALLY_MODIFIED_AND_STALE;
                 } else if (modified) {
-                    return new StandardVersionedFlowStatus(VersionedFlowState.LOCALLY_MODIFIED, "Local changes have been made", null);
+                    flowState = VersionedFlowState.LOCALLY_MODIFIED;
                 } else if (stale) {
-                    return new StandardVersionedFlowStatus(VersionedFlowState.STALE, "A newer version of this flow is available", null);
+                    flowState = VersionedFlowState.STALE;
                 } else {
-                    return new StandardVersionedFlowStatus(VersionedFlowState.UP_TO_DATE, "Flow version is current", null);
+                    flowState = VersionedFlowState.UP_TO_DATE;
                 }
+
+                return new StandardVersionedFlowStatus(flowState, flowState.getDescription());
             }
         };
 
@@ -3031,11 +3018,23 @@ public final class StandardProcessGroup implements ProcessGroup {
         svci.setFlowName(versionControlInformation.getFlowName());
         svci.setFlowDescription(versionControlInformation.getFlowDescription());
 
+        final VersionedFlowState flowState = versionControlInformation.getStatus().getState();
+        versionControlFields.setStale(flowState == VersionedFlowState.STALE || flowState == VersionedFlowState.LOCALLY_MODIFIED_AND_STALE);
+        versionControlFields.setLocallyModified(flowState == VersionedFlowState.LOCALLY_MODIFIED || flowState == VersionedFlowState.LOCALLY_MODIFIED_AND_STALE);
+        versionControlFields.setSyncFailureExplanation(null);
+
         writeLock.lock();
         try {
             updateVersionedComponentIds(this, versionedComponentIds);
             this.versionControlInfo.set(svci);
-            clearFlowDifferences();
+            versionControlFields.setFlowDifferences(null);
+
+            final ProcessGroup parent = getParent();
+            if (parent != null) {
+                parent.onComponentModified();
+            }
+
+            scheduler.submitFrameworkTask(() -> synchronizeWithFlowRegistry(flowController.getFlowRegistryClient()));
         } finally {
             writeLock.unlock();
         }
@@ -3156,14 +3155,6 @@ public final class StandardProcessGroup implements ProcessGroup {
             .forEach(childGroup -> applyVersionedComponentIds(childGroup, lookup));
     }
 
-    private void setSyncFailedState(final String explanation) {
-        boolean updated = false;
-        while (!updated) {
-            final StandardVersionedFlowStatus status = flowStatus.get();
-            final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(VersionedFlowState.SYNC_FAILURE, explanation, status.getCurrentDifferences());
-            updated = flowStatus.compareAndSet(status, updatedStatus);
-        }
-    }
 
     @Override
     public void synchronizeWithFlowRegistry(final FlowRegistryClient flowRegistryClient) {
@@ -3177,7 +3168,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         if (flowRegistry == null) {
             final String message = String.format("Unable to synchronize Process Group with Flow Registry because Process Group was placed under Version Control using Flow Registry "
                 + "with identifier %s but cannot find any Flow Registry with this identifier", registryId);
-            setSyncFailedState(message);
+            versionControlFields.setSyncFailureExplanation(message);
 
             LOG.error("Unable to synchronize {} with Flow Registry because Process Group was placed under Version Control using Flow Registry "
                 + "with identifier {} but cannot find any Flow Registry with this identifier", this, registryId);
@@ -3195,7 +3186,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             } catch (final IOException | NiFiRegistryException e) {
                 final String message = String.format("Failed to synchronize Process Group with Flow Registry because could not retrieve version %s of flow with identifier %s in bucket %s",
                     vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier());
-                setSyncFailedState(message);
+                versionControlFields.setSyncFailureExplanation(message);
 
                 LOG.error("Failed to synchronize {} with Flow Registry because could not retrieve version {} of flow with identifier {} in bucket {}",
                     this, vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier(), e);
@@ -3213,22 +3204,17 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             if (latestVersion == vci.getVersion()) {
                 LOG.debug("{} is currently at the most recent version ({}) of the flow that is under Version Control", this, latestVersion);
-                vci.setCurrent(true);
+                versionControlFields.setStale(false);
             } else {
-                vci.setCurrent(false);
                 LOG.info("{} is not the most recent version of the flow that is under Version Control; current version is {}; most recent version is {}",
                     new Object[] {this, vci.getVersion(), latestVersion});
+                versionControlFields.setStale(true);
             }
 
-            boolean updated = false;
-            while (!updated) {
-                final StandardVersionedFlowStatus status = flowStatus.get();
-                final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(null, null, status.getCurrentDifferences());
-                updated = flowStatus.compareAndSet(status, updatedStatus);
-            }
+            versionControlFields.setSyncFailureExplanation(null);
         } catch (final IOException | NiFiRegistryException e) {
             final String message = String.format("Failed to synchronize Process Group with Flow Registry : " + e.getMessage());
-            setSyncFailedState(message);
+            versionControlFields.setSyncFailureExplanation(message);
 
             LOG.error("Failed to synchronize {} with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry", this, e);
         }
@@ -3253,10 +3239,6 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             final Set<String> updatedVersionedComponentIds = new HashSet<>();
             for (final FlowDifference diff : flowComparison.getDifferences()) {
-                if (diff.getDifferenceType() == DifferenceType.POSITION_CHANGED) {
-                    continue;
-                }
-
                 // If this update adds a new Controller Service, then we need to check if the service already exists at a higher level
                 // and if so compare our VersionedControllerService to the existing service.
                 if (diff.getDifferenceType() == DifferenceType.COMPONENT_ADDED) {
@@ -3393,6 +3375,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             final FlowRegistry flowRegistry = flowController.getFlowRegistryClient().getFlowRegistry(registryId);
             final String registryName = flowRegistry == null ? registryId : flowRegistry.getName();
 
+            final VersionedFlowState flowState = remoteCoordinates.getLatest() ? VersionedFlowState.UP_TO_DATE : VersionedFlowState.STALE;
+
             final VersionControlInformation vci = new StandardVersionControlInformation.Builder()
                 .registryId(registryId)
                 .registryName(registryName)
@@ -3402,8 +3386,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 .flowName(flowId)
                 .version(version)
                 .flowSnapshot(proposed)
-                .modified(false)
-                .current(remoteCoordinates.getLatest())
+                .status(new StandardVersionedFlowStatus(flowState, flowState.getDescription()))
                 .build();
 
             group.setVersionControlInformation(vci, Collections.emptyMap());
@@ -4149,13 +4132,9 @@ public final class StandardProcessGroup implements ProcessGroup {
         final FlowComparator flowComparator = new StandardFlowComparator(snapshotFlow, currentFlow, getAncestorGroupServiceIds(), new EvolvingDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();
         final Set<FlowDifference> differences = comparison.getDifferences();
-        final Set<FlowDifference> functionalDifferences = differences.stream()
-            .filter(diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED)
-            .filter(diff -> diff.getDifferenceType() != DifferenceType.STYLE_CHANGED)
-            .collect(Collectors.toSet());
 
         LOG.debug("There are {} differences between this Local Flow and the Versioned Flow: {}", differences.size(), differences);
-        return functionalDifferences;
+        return differences;
     }
 
 
@@ -4170,7 +4149,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 }
 
                 if (verifyNotDirty) {
-                    final boolean modified = versionControlInfo.isModified();
+                    final VersionedFlowState flowState = versionControlInfo.getStatus().getState();
+                    final boolean modified = flowState == VersionedFlowState.LOCALLY_MODIFIED || flowState == VersionedFlowState.LOCALLY_MODIFIED_AND_STALE;
 
                     final Set<FlowDifference> modifications = getModifications();
 
@@ -4186,7 +4166,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                         throw new IllegalStateException("Cannot change the Version of the flow for " + this
                             + " because the Process Group has been modified (" + modifications.size()
                             + " modifications) since it was last synchronized with the Flow Registry. The Process Group must be"
-                            + " reverted to its original form before changing the version. See logs for more information on what has changed.");
+                            + " reverted to its original form before changing the version.");
                     }
                 }
             }
@@ -4393,8 +4373,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             if (flowId != null && flowId.equals(vci.getFlowIdentifier())) {
                 // Flow ID is the same. We want to publish the Process Group as the next version of the Flow.
                 // In order to do this, we have to ensure that the Process Group is 'current'.
-                final boolean current = vci.isCurrent();
-                if (!current) {
+                final VersionedFlowState state = vci.getStatus().getState();
+                if (state == VersionedFlowState.STALE || state == VersionedFlowState.LOCALLY_MODIFIED_AND_STALE) {
                     throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + getIdentifier()
                         + " because the Process Group in the flow is not synchronized with the most recent version of the Flow in the Flow Registry. "
                         + "In order to publish a new version of the Flow, the Process Group must first be in synch with the latest version in the Flow Registry.");
@@ -4441,10 +4421,15 @@ public final class StandardProcessGroup implements ProcessGroup {
     private void verifyNoDescendantsWithLocalModifications(final String action) {
         for (final ProcessGroup descendant : findAllProcessGroups()) {
             final VersionControlInformation descendantVci = descendant.getVersionControlInformation();
-            if (descendantVci != null && descendantVci.isModified()) {
-                throw new IllegalStateException("Process Group cannot " + action + " because it contains a child or descendant Process Group that is under Version Control and "
-                    + "has local modifications. Each descendant Process Group that is under Version Control must first be reverted or have its changes pushed to the Flow Registry before "
-                    + "this action can be performed on the parent Process Group.");
+            if (descendantVci != null) {
+                final VersionedFlowState flowState = descendantVci.getStatus().getState();
+                final boolean modified = flowState == VersionedFlowState.LOCALLY_MODIFIED || flowState == VersionedFlowState.LOCALLY_MODIFIED_AND_STALE;
+
+                if (modified) {
+                    throw new IllegalStateException("Process Group cannot " + action + " because it contains a child or descendant Process Group that is under Version Control and "
+                        + "has local modifications. Each descendant Process Group that is under Version Control must first be reverted or have its changes pushed to the Flow Registry before "
+                        + "this action can be performed on the parent Process Group.");
+                }
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
index f362c1e..4be9898 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
@@ -17,21 +17,16 @@
 
 package org.apache.nifi.groups;
 
-import java.util.Set;
-
 import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.registry.flow.VersionedFlowStatus;
-import org.apache.nifi.registry.flow.diff.FlowDifference;
 
 class StandardVersionedFlowStatus implements VersionedFlowStatus {
     private final VersionedFlowState state;
     private final String explanation;
-    private final Set<FlowDifference> currentDifferences;
 
-    StandardVersionedFlowStatus(final VersionedFlowState state, final String explanation, final Set<FlowDifference> differences) {
+    StandardVersionedFlowStatus(final VersionedFlowState state, final String explanation) {
         this.state = state;
         this.explanation = explanation;
-        this.currentDifferences = differences;
     }
 
     @Override
@@ -43,8 +38,4 @@ class StandardVersionedFlowStatus implements VersionedFlowStatus {
     public String getStateExplanation() {
         return explanation;
     }
-
-    Set<FlowDifference> getCurrentDifferences() {
-        return currentDifferences;
-    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/VersionControlFields.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/VersionControlFields.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/VersionControlFields.java
new file mode 100644
index 0000000..50c640b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/VersionControlFields.java
@@ -0,0 +1,61 @@
+/*
+ * 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.groups;
+
+import java.util.Set;
+
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+
+public class VersionControlFields {
+    private volatile boolean locallyModified;
+    private volatile boolean stale;
+    private volatile String syncFailureExplanation = "Not yet synchronized with Flow Registry";
+    private volatile Set<FlowDifference> flowDifferences;
+
+    boolean isLocallyModified() {
+        return locallyModified;
+    }
+
+    void setLocallyModified(final boolean locallyModified) {
+        this.locallyModified = locallyModified;
+    }
+
+    boolean isStale() {
+        return stale;
+    }
+
+    void setStale(final boolean stale) {
+        this.stale = stale;
+    }
+
+    String getSyncFailureExplanation() {
+        return syncFailureExplanation;
+    }
+
+    void setSyncFailureExplanation(final String syncFailureExplanation) {
+        this.syncFailureExplanation = syncFailureExplanation;
+    }
+
+    Set<FlowDifference> getFlowDifferences() {
+        return flowDifferences;
+    }
+
+    void setFlowDifferences(final Set<FlowDifference> flowDifferences) {
+        this.flowDifferences = flowDifferences;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
index 106d19a..feef0e0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
@@ -32,8 +32,6 @@ public class StandardVersionControlInformation implements VersionControlInformat
     private volatile String flowDescription;
     private final int version;
     private volatile VersionedProcessGroup flowSnapshot;
-    private volatile boolean modified;
-    private volatile boolean current;
     private final VersionedFlowStatus status;
 
     public static class Builder {
@@ -46,8 +44,6 @@ public class StandardVersionControlInformation implements VersionControlInformat
         private String flowDescription;
         private int version;
         private VersionedProcessGroup flowSnapshot;
-        private Boolean modified = null;
-        private Boolean current = null;
         private VersionedFlowStatus status;
 
         public Builder registryId(String registryId) {
@@ -90,16 +86,6 @@ public class StandardVersionControlInformation implements VersionControlInformat
             return this;
         }
 
-        public Builder modified(boolean modified) {
-            this.modified = modified;
-            return this;
-        }
-
-        public Builder current(boolean current) {
-            this.current = current;
-            return this;
-        }
-
         public Builder flowSnapshot(VersionedProcessGroup snapshot) {
             this.flowSnapshot = snapshot;
             return this;
@@ -119,8 +105,17 @@ public class StandardVersionControlInformation implements VersionControlInformat
                 .flowId(dto.getFlowId())
                 .flowName(dto.getFlowName())
                 .flowDescription(dto.getFlowDescription())
-                .current(dto.getCurrent() == null ? true : dto.getCurrent())
-                .modified(dto.getModified() == null ? false : dto.getModified())
+                .status(new VersionedFlowStatus() {
+                    @Override
+                    public VersionedFlowState getState() {
+                        return VersionedFlowState.valueOf(dto.getState());
+                    }
+
+                    @Override
+                    public String getStateExplanation() {
+                        return dto.getStateExplanation();
+                    }
+                })
                 .version(dto.getVersion());
 
             return builder;
@@ -133,7 +128,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
             Objects.requireNonNull(version, "Version must be specified");
 
             final StandardVersionControlInformation svci = new StandardVersionControlInformation(registryIdentifier, registryName,
-                bucketIdentifier, flowIdentifier, version, flowSnapshot, modified, current, status);
+                bucketIdentifier, flowIdentifier, version, flowSnapshot, status);
 
             svci.setBucketName(bucketName);
             svci.setFlowName(flowName);
@@ -145,15 +140,13 @@ public class StandardVersionControlInformation implements VersionControlInformat
 
 
     public StandardVersionControlInformation(final String registryId, final String registryName, final String bucketId, final String flowId, final int version,
-        final VersionedProcessGroup snapshot, final boolean modified, final boolean current, final VersionedFlowStatus status) {
+        final VersionedProcessGroup snapshot, final VersionedFlowStatus status) {
         this.registryIdentifier = registryId;
         this.registryName = registryName;
         this.bucketIdentifier = bucketId;
         this.flowIdentifier = flowId;
         this.version = version;
         this.flowSnapshot = snapshot;
-        this.modified = modified;
-        this.current = current;
         this.status = status;
     }
 
@@ -215,28 +208,10 @@ public class StandardVersionControlInformation implements VersionControlInformat
     }
 
     @Override
-    public boolean isModified() {
-        return modified;
-    }
-
-    @Override
-    public boolean isCurrent() {
-        return current;
-    }
-
-    @Override
     public VersionedProcessGroup getFlowSnapshot() {
         return flowSnapshot;
     }
 
-    public void setModified(final boolean modified) {
-        this.modified = modified;
-    }
-
-    public void setCurrent(final boolean current) {
-        this.current = current;
-    }
-
     public void setFlowSnapshot(final VersionedProcessGroup flowSnapshot) {
         this.flowSnapshot = flowSnapshot;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index 5808500..0b9c6f2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -178,9 +178,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
             }
         };
 
-        final Runnable checkAuthorizations = new InitializationTask();
         backgroundThreadExecutor = new FlowEngine(1, "Remote Process Group " + id, true);
-        backgroundThreadExecutor.scheduleWithFixedDelay(checkAuthorizations, 30L, 30L, TimeUnit.SECONDS);
     }
 
     @Override
@@ -197,6 +195,9 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
                 logger.warn("Unable to communicate with remote instance {}", new Object[] {this, e});
             }
         });
+
+        final Runnable checkAuthorizations = new InitializationTask();
+        backgroundThreadExecutor.scheduleWithFixedDelay(checkAuthorizations, 0L, 60L, TimeUnit.SECONDS);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 78335f4..514cd18 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -1442,20 +1442,6 @@ public interface NiFiServiceFacade {
      */
     void verifyCanRevertLocalModifications(String groupId, VersionedFlowSnapshot versionedFlowSnapshot);
 
-    /**
-     * Updates the Process group with the given ID to match the new snapshot
-     *
-     * @param revision the revision of the Process Group
-     * @param groupId the ID of the Process Group
-     * @param versionControlInfo the Version Control information
-     * @param snapshot the new snapshot
-     * @param componentIdSeed the seed to use for generating new component ID's
-     * @param updateDescendantVersionedFlows if a child/descendant Process Group is under Version Control, specifies whether or not to
-     *            update the contents of that Process Group
-     * @return the Process Group
-     */
-    ProcessGroupEntity updateProcessGroup(Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
-        boolean verifyNotModified, boolean updateDescendantVersionedFlows);
 
     /**
      * Updates the Process group with the given ID to match the new snapshot

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index ae89ef0..a2d6e41 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -97,6 +97,7 @@ import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.ConciseEvolvingDifferenceDescriptor;
@@ -292,6 +293,7 @@ import java.util.UUID;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Implementation of NiFiServiceFacade that performs revision checking.
@@ -1592,7 +1594,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
             final D dto = dtoCreation.apply(component);
             final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
-            return new StandardRevisionUpdate<D>(dto, lastMod);
+            return new StandardRevisionUpdate<>(dto, lastMod);
         });
     }
 
@@ -1779,7 +1781,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         controllerFacade.save();
 
         final SnippetDTO dto = dtoFactory.createSnippetDto(snippet);
-        final RevisionUpdate<SnippetDTO> snapshot = new StandardRevisionUpdate<SnippetDTO>(dto, null);
+        final RevisionUpdate<SnippetDTO> snapshot = new StandardRevisionUpdate<>(dto, null);
 
         return entityFactory.createSnippetEntity(snapshot.getComponent());
     }
@@ -2088,7 +2090,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                     controllerFacade.save();
 
                     final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
-                    return new StandardRevisionUpdate<ControllerServiceDTO>(dto, lastMod);
+                    return new StandardRevisionUpdate<>(dto, lastMod);
                 });
         } else {
             snapshot = revisionManager.updateRevision(claim, user, () -> {
@@ -2098,7 +2100,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                 controllerFacade.save();
 
                 final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
-                return new StandardRevisionUpdate<ControllerServiceDTO>(dto, lastMod);
+                return new StandardRevisionUpdate<>(dto, lastMod);
             });
         }
 
@@ -2440,7 +2442,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             final Revision updatedRevision = revisionManager.getRevision(revision.getComponentId()).incrementRevision(revision.getClientId());
             final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity());
 
-            return new StandardRevisionUpdate<FlowRegistry>(registry, lastModification);
+            return new StandardRevisionUpdate<>(registry, lastModification);
         });
 
         final FlowRegistry updatedReg = revisionUpdate.getComponent();
@@ -2483,7 +2485,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
             final ReportingTaskDTO dto = dtoFactory.createReportingTaskDto(reportingTask);
             final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
-            return new StandardRevisionUpdate<ReportingTaskDTO>(dto, lastMod);
+            return new StandardRevisionUpdate<>(dto, lastMod);
         });
 
         final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskDTO.getId());
@@ -3649,6 +3651,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     @Override
     public VersionControlComponentMappingEntity registerFlowWithFlowRegistry(final String groupId, final StartVersionControlRequestEntity requestEntity) {
         final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+
         final VersionControlInformation currentVci = processGroup.getVersionControlInformation();
         final int expectedVersion = currentVci == null ? 1 : currentVci.getVersion() + 1;
 
@@ -3697,15 +3700,14 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final VersionControlInformationDTO vci = new VersionControlInformationDTO();
         vci.setBucketId(bucket.getIdentifier());
         vci.setBucketName(bucket.getName());
-        vci.setCurrent(true);
         vci.setFlowId(flow.getIdentifier());
         vci.setFlowName(flow.getName());
         vci.setFlowDescription(flow.getDescription());
         vci.setGroupId(groupId);
-        vci.setModified(false);
         vci.setRegistryId(registryId);
         vci.setRegistryName(getFlowRegistryName(registryId));
         vci.setVersion(registeredSnapshot.getSnapshotMetadata().getVersion());
+        vci.setState(VersionedFlowState.UP_TO_DATE.name());
 
         final Map<String, String> mapping = dtoFactory.createVersionControlComponentMappingDto(versionedProcessGroup);
 
@@ -3777,8 +3779,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, ancestorServiceIds, new ConciseEvolvingDifferenceDescriptor());
         final FlowComparison flowComparison = flowComparator.compare();
 
-        final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison,
-            diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED && diff.getDifferenceType() != DifferenceType.STYLE_CHANGED);
+        final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison);
 
         final FlowComparisonEntity entity = new FlowComparisonEntity();
         entity.setComponentDifferences(differenceDtos);
@@ -4079,30 +4080,88 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return flowRegistry == null ? flowRegistryId : flowRegistry.getName();
     }
 
-    @Override
-    public ProcessGroupEntity updateProcessGroup(final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
-        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateDescendantVersionedFlows) {
+    private List<Revision> getComponentRevisions(final ProcessGroup processGroup, final boolean includeGroupRevision) {
+        final List<Revision> revisions = new ArrayList<>();
+        if (includeGroupRevision) {
+            revisions.add(revisionManager.getRevision(processGroup.getIdentifier()));
+        }
 
-        final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        return updateProcessGroupContents(user, revision, groupId, versionControlInfo, proposedFlowSnapshot, componentIdSeed, verifyNotModified, true, updateDescendantVersionedFlows);
+        processGroup.findAllConnections().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllControllerServices().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllFunnels().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllInputPorts().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllOutputPorts().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllLabels().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllProcessGroups().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllProcessors().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllRemoteProcessGroups().stream()
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+        processGroup.findAllRemoteProcessGroups().stream()
+            .flatMap(rpg -> Stream.concat(rpg.getInputPorts().stream(), rpg.getOutputPorts().stream()))
+            .map(component -> revisionManager.getRevision(component.getIdentifier()))
+            .forEach(revisions::add);
+
+        return revisions;
     }
 
     @Override
     public ProcessGroupEntity updateProcessGroupContents(final NiFiUser user, final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
         final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) {
 
-        final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(groupId);
-        final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(user, revision,
-            processGroupNode,
-            () -> processGroupDAO.updateProcessGroupFlow(groupId, user, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows),
-            processGroup -> dtoFactory.createProcessGroupDto(processGroup));
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final List<Revision> revisions = getComponentRevisions(processGroup, false);
+        revisions.add(revision);
 
-        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);
-        final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
-        final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(processGroupNode.getIdentifier()));
-        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroupNode.getIdentifier()));
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revisions);
+
+        final RevisionUpdate<ProcessGroupDTO> revisionUpdate = revisionManager.updateRevision(revisionClaim, user, new UpdateRevisionTask<ProcessGroupDTO>() {
+            @Override
+            public RevisionUpdate<ProcessGroupDTO> update() {
+                // update the Process Group
+                processGroupDAO.updateProcessGroupFlow(groupId, user, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows);
+
+                // update the revisions
+                final Set<Revision> updatedRevisions = revisions.stream()
+                    .map(rev -> revisionManager.getRevision(rev.getComponentId()).incrementRevision(revision.getClientId()))
+                    .collect(Collectors.toSet());
+
+                // save
+                controllerFacade.save();
+
+                // gather details for response
+                final ProcessGroupDTO dto = dtoFactory.createProcessGroupDto(processGroup);
+
+                final Revision updatedRevision = revisionManager.getRevision(groupId).incrementRevision(revision.getClientId());
+                final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity());
+                return new StandardRevisionUpdate<>(dto, lastModification, updatedRevisions);
+            }
+        });
+
+        final FlowModification lastModification = revisionUpdate.getLastModification();
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+        final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(lastModification);
+        final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(processGroup.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroup.getIdentifier()));
         final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
-        return entityFactory.createProcessGroupEntity(snapshot.getComponent(), updatedRevision, permissions, status, bulletinEntities);
+        return entityFactory.createProcessGroupEntity(revisionUpdate.getComponent(), updatedRevision, permissions, status, bulletinEntities);
     }
 
     private AuthorizationResult authorizeAction(final Action action) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index a3bb5b2..b3ccefb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -16,12 +16,32 @@
  */
 package org.apache.nifi.web.api;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeAccess;
@@ -47,6 +67,7 @@ import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.FlowRegistryUtils;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateStep;
 import org.apache.nifi.remote.util.SiteToSiteRestApiClient;
@@ -111,31 +132,6 @@ import org.slf4j.LoggerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.stream.XMLStreamReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
@@ -161,6 +157,13 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+
 /**
  * RESTful endpoint for managing a Group.
  */
@@ -1657,8 +1660,8 @@ public class ProcessGroupResource extends ApplicationResource {
             versionControlInfo.setFlowDescription(flow.getDescription());
 
             versionControlInfo.setRegistryName(serviceFacade.getFlowRegistryName(versionControlInfo.getRegistryId()));
-            versionControlInfo.setModified(false);
-            versionControlInfo.setCurrent(flowSnapshot.isLatest());
+            final VersionedFlowState flowState = flowSnapshot.isLatest() ? VersionedFlowState.UP_TO_DATE : VersionedFlowState.STALE;
+            versionControlInfo.setState(flowState.name());
 
             // Step 3: Resolve Bundle info
             BundleUtils.discoverCompatibleBundles(flowSnapshot.getFlowContents());
@@ -1689,8 +1692,12 @@ public class ProcessGroupResource extends ApplicationResource {
                         }
                     }
                 },
-            () -> {
-            },
+                () -> {
+                    final VersionedFlowSnapshot versionedFlowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
+                    if (versionedFlowSnapshot != null) {
+                        serviceFacade.verifyComponentTypes(versionedFlowSnapshot.getFlowContents());
+                    }
+                },
                 processGroupEntity -> {
                     final ProcessGroupDTO processGroup = processGroupEntity.getComponent();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 5dc7325..950bd97 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -40,6 +40,7 @@ import org.apache.nifi.registry.flow.FlowRegistryUtils;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.util.BundleUtils;
 import org.apache.nifi.web.NiFiServiceFacade;
@@ -166,14 +167,14 @@ public class VersionsResource extends ApplicationResource {
 
 
     @POST
-    @Consumes(MediaType.WILDCARD)
-    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.TEXT_PLAIN)
     @Path("active-requests")
     @ApiOperation(
         value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed. Creating this request will "
             + "prevent any other threads from simultaneously saving local changes to Version Control. It will not, however, actually save the local flow to the Flow Registry. A "
             + "POST to /versions/process-groups/{id} should be used to initiate saving of the local flow to the Flow Registry.",
-            response = VersionControlInformationEntity.class,
+            response = String.class,
             notes = NON_GUARANTEED_ENDPOINT)
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -186,7 +187,7 @@ public class VersionsResource extends ApplicationResource {
             @ApiParam(value = "The versioned flow details.", required = true) final CreateActiveRequestEntity requestEntity) {
 
         if (isReplicateRequest()) {
-            return replicate(HttpMethod.POST);
+            return replicate(HttpMethod.POST, requestEntity);
         }
 
         if (requestEntity.getProcessGroupId() == null) {
@@ -548,11 +549,14 @@ public class VersionsResource extends ApplicationResource {
             final CreateActiveRequestEntity activeRequestEntity = new CreateActiveRequestEntity();
             activeRequestEntity.setProcessGroupId(groupId);
 
+            final Map<String, String> headers = new HashMap<>();
+            headers.put("content-type", MediaType.APPLICATION_JSON);
+
             if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, activeRequestEntity, Collections.emptyMap()).awaitMergedResponse();
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, activeRequestEntity, headers).awaitMergedResponse();
             } else {
                 clusterResponse = getRequestReplicator().forwardToCoordinator(
-                    getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, activeRequestEntity, Collections.emptyMap()).awaitMergedResponse();
+                    getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, activeRequestEntity, headers).awaitMergedResponse();
             }
         } catch (final InterruptedException ie) {
             Thread.currentThread().interrupt();
@@ -761,18 +765,20 @@ public class VersionsResource extends ApplicationResource {
                 final VersionControlInformationDTO versionControlInfoDto = new VersionControlInformationDTO();
                 versionControlInfoDto.setBucketId(snapshotMetadata.getBucketIdentifier());
                 versionControlInfoDto.setBucketName(bucket.getName());
-                versionControlInfoDto.setCurrent(snapshotMetadata.getVersion() == flow.getVersionCount());
                 versionControlInfoDto.setFlowId(snapshotMetadata.getFlowIdentifier());
                 versionControlInfoDto.setFlowName(flow.getName());
                 versionControlInfoDto.setFlowDescription(flow.getDescription());
                 versionControlInfoDto.setGroupId(groupId);
-                versionControlInfoDto.setModified(false);
                 versionControlInfoDto.setVersion(snapshotMetadata.getVersion());
                 versionControlInfoDto.setRegistryId(entity.getRegistryId());
                 versionControlInfoDto.setRegistryName(serviceFacade.getFlowRegistryName(entity.getRegistryId()));
 
-                final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroup(rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false,
-                    entity.getUpdateDescendantVersionedFlows());
+                final VersionedFlowState flowState = snapshotMetadata.getVersion() == flow.getVersionCount() ? VersionedFlowState.UP_TO_DATE : VersionedFlowState.STALE;
+                versionControlInfoDto.setState(flowState.name());
+
+                final NiFiUser user = NiFiUserUtils.getNiFiUser();
+                final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroupContents(user, rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false,
+                    true, entity.getUpdateDescendantVersionedFlows());
                 final VersionControlInformationDTO updatedVci = updatedGroup.getComponent().getVersionControlInformation();
 
                 final VersionControlInformationEntity responseEntity = new VersionControlInformationEntity();
@@ -1103,7 +1109,7 @@ public class VersionsResource extends ApplicationResource {
                             affectedComponents, user, replicateRequest, processGroupEntity, flowSnapshot, request, idGenerationSeed, true, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
-                    } catch (final LifecycleManagementException e) {
+                    } catch (final Exception e) {
                         logger.error("Failed to update flow to new version", e);
                         vcur.setFailureReason("Failed to update flow to new version due to " + e);
                     }
@@ -1268,7 +1274,7 @@ public class VersionsResource extends ApplicationResource {
                             affectedComponents, user, replicateRequest, processGroupEntity, flowSnapshot, request, idGenerationSeed, false, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
-                    } catch (final LifecycleManagementException e) {
+                    } catch (final Exception e) {
                         logger.error("Failed to update flow to new version", e);
                         vcur.setFailureReason("Failed to update flow to new version due to " + e.getMessage());
                     }
@@ -1403,15 +1409,14 @@ public class VersionsResource extends ApplicationResource {
                 final VersionControlInformationDTO vci = new VersionControlInformationDTO();
                 vci.setBucketId(metadata.getBucketIdentifier());
                 vci.setBucketName(bucket.getName());
-                vci.setCurrent(flowSnapshot.isLatest());
                 vci.setFlowDescription(flow.getDescription());
                 vci.setFlowId(flow.getIdentifier());
                 vci.setFlowName(flow.getName());
                 vci.setGroupId(groupId);
-                vci.setModified(false);
                 vci.setRegistryId(requestVci.getRegistryId());
                 vci.setRegistryName(serviceFacade.getFlowRegistryName(requestVci.getRegistryId()));
                 vci.setVersion(metadata.getVersion());
+                vci.setState(flowSnapshot.isLatest() ? VersionedFlowState.UP_TO_DATE.name() : VersionedFlowState.STALE.name());
 
                 serviceFacade.updateProcessGroupContents(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified, false, updateDescendantVersionedFlows);
             }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index ca781a4..5bdb040 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.web.api.dto;
 
+import javax.ws.rs.WebApplicationException;
+
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -114,7 +116,6 @@ import org.apache.nifi.provenance.lineage.LineageNode;
 import org.apache.nifi.provenance.lineage.ProvenanceEventLineageNode;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.flow.FlowRegistry;
-import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedFlowState;
@@ -140,7 +141,6 @@ import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.reporting.ReportingTask;
 import org.apache.nifi.scheduling.SchedulingStrategy;
 import org.apache.nifi.util.FormatUtils;
-import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.FlowModification;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.dto.action.ActionDTO;
@@ -192,7 +192,6 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
-import javax.ws.rs.WebApplicationException;
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -215,7 +214,6 @@ import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
-import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -234,8 +232,6 @@ public final class DtoFactory {
     private ControllerServiceProvider controllerServiceProvider;
     private EntityFactory entityFactory;
     private Authorizer authorizer;
-    private NiFiProperties properties;
-    private FlowRegistryClient flowRegistryClient;
 
     public ControllerConfigurationDTO createControllerConfigurationDto(final ControllerFacade controllerFacade) {
         final ControllerConfigurationDTO dto = new ControllerConfigurationDTO();
@@ -2190,23 +2186,17 @@ public final class DtoFactory {
 
 
     public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison) {
-        return createComponentDifferenceDtos(comparison, null);
-    }
-
-    public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison, final Predicate<FlowDifference> filter) {
         final Map<ComponentDifferenceDTO, List<DifferenceDTO>> differencesByComponent = new HashMap<>();
 
         for (final FlowDifference difference : comparison.getDifferences()) {
-            if (filter == null || filter.test(difference)) {
-                final ComponentDifferenceDTO componentDiff = createComponentDifference(difference);
-                final List<DifferenceDTO> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
+            final ComponentDifferenceDTO componentDiff = createComponentDifference(difference);
+            final List<DifferenceDTO> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
 
-                final DifferenceDTO dto = new DifferenceDTO();
-                dto.setDifferenceType(difference.getDifferenceType().getDescription());
-                dto.setDifference(difference.getDescription());
+            final DifferenceDTO dto = new DifferenceDTO();
+            dto.setDifferenceType(difference.getDifferenceType().getDescription());
+            dto.setDifference(difference.getDescription());
 
-                differences.add(dto);
-            }
+            differences.add(dto);
         }
 
         for (final Map.Entry<ComponentDifferenceDTO, List<DifferenceDTO>> entry : differencesByComponent.entrySet()) {
@@ -2259,8 +2249,6 @@ public final class DtoFactory {
         dto.setFlowName(versionControlInfo.getFlowName());
         dto.setFlowDescription(versionControlInfo.getFlowDescription());
         dto.setVersion(versionControlInfo.getVersion());
-        dto.setCurrent(versionControlInfo.isCurrent());
-        dto.setModified(versionControlInfo.isModified());
 
         final VersionedFlowStatus status = versionControlInfo.getStatus();
         final VersionedFlowState state = status.getState();
@@ -3501,8 +3489,6 @@ public final class DtoFactory {
         copy.setFlowName(original.getFlowName());
         copy.setFlowDescription(original.getFlowDescription());
         copy.setVersion(original.getVersion());
-        copy.setCurrent(original.getCurrent());
-        copy.setModified(original.getModified());
         copy.setState(original.getState());
         copy.setStateExplanation(original.getStateExplanation());
         return copy;
@@ -3833,12 +3819,4 @@ public final class DtoFactory {
     public void setBulletinRepository(BulletinRepository bulletinRepository) {
         this.bulletinRepository = bulletinRepository;
     }
-
-    public void setProperties(final NiFiProperties properties) {
-        this.properties = properties;
-    }
-
-    public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
-        this.flowRegistryClient = flowRegistryClient;
-    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index d25f294..52a18dc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -26,6 +26,7 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.VersionControlInformation;
@@ -234,6 +235,10 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         }
         if (isNotNull(processGroupDTO.getPosition())) {
             group.setPosition(new Position(processGroupDTO.getPosition().getX(), processGroupDTO.getPosition().getY()));
+            final ProcessGroup parent = group.getParent();
+            if (parent != null) {
+                parent.onComponentModified();
+            }
         }
         if (isNotNull(comments)) {
             group.setComments(comments);
@@ -258,8 +263,6 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         final StandardVersionControlInformation vci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
             .registryName(registryName)
             .flowSnapshot(flowSnapshot)
-            .modified(false)
-            .current(true)
             .build();
 
         group.setVersionControlInformation(vci, versionedComponentMapping);
@@ -281,6 +284,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
 
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
         group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows);
+        group.findAllRemoteProcessGroups().stream().forEach(RemoteProcessGroup::initialize);
 
         final StandardVersionControlInformation svci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
             .flowSnapshot(proposedSnapshot.getFlowContents())


[06/50] nifi git commit: NIFI-4436: - Adding support to save a version of a flow based on a selected Process Group. - Adding support for revert changes back to the most recent version. - Adding support to disconnect from version control. - Moving the ver

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
index 8fb73a8..8f8bb6d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
@@ -230,7 +230,7 @@
                 applyFilter();
             });
 
-            // initialize the processor configuration dialog
+            // initialize the component state dialog
             $('#component-state-dialog').modal({
                 scrollableContentStyle: 'scrollable',
                 headerText: 'Component State',

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
index 3110e64..02abac1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
@@ -18,7 +18,7 @@
 /* global nf */
 
 /**
- * Views state for a given component.
+ * Handles changing the version of a component bundle.
  */
 (function (root, factory) {
     if (typeof define === 'function' && define.amd) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index a32a47f..f128086 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -97,7 +97,7 @@
      * @param {selection} selection         The selection of currently selected components
      */
     var canCreateTemplate = function (selection) {
-        return nfCanvasUtils.canWrite() && (selection.empty() || nfCanvasUtils.canRead(selection));
+        return nfCanvasUtils.canWriteCurrentGroup() && (selection.empty() || nfCanvasUtils.canRead(selection));
     };
 
     /**
@@ -106,7 +106,7 @@
      * @param {selection} selection         The selection of currently selected components
      */
     var canUploadTemplate = function (selection) {
-        return nfCanvasUtils.canWrite() && selection.empty();
+        return nfCanvasUtils.canWriteCurrentGroup() && selection.empty();
     };
 
     /**
@@ -370,6 +370,89 @@
     };
 
     /**
+     * Determines whether the current selection supports flow versioning.
+     *
+     * @param selection
+     */
+    var supportsFlowVersioning = function (selection) {
+        if (nfCanvasUtils.supportsFlowVersioning() === false) {
+            return false;
+        }
+
+        if (selection.empty()) {
+            return nfCanvasUtils.canReadCurrentGroup() && nfCanvasUtils.canWriteCurrentGroup();
+        }
+
+        if (isProcessGroup(selection) === true) {
+            return nfCanvasUtils.canRead(selection) && nfCanvasUtils.canModify(selection);
+        }
+
+        return false;
+    };
+
+    /**
+     * Determines whether the current selection supports starting flow versioning.
+     *
+     * @param selection
+     */
+    var supportsStartFlowVersioning = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    return nfCommon.isUndefinedOrNull(breadcrumbEntity.breadcrumb.versionControlInformation);
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        // check the selection for version control information
+        var processGroupData = selection.datum();
+        return nfCommon.isUndefinedOrNull(processGroupData.component.versionControlInformation);
+    };
+
+    /**
+     * Determines whether the current selection supports stopping flow versioning.
+     *
+     * @param selection
+     */
+    var supportsStopFlowVersioning = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    return nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation);
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        // check the selection for version control information
+        var processGroupData = selection.datum();
+        return nfCommon.isDefinedAndNotNull(processGroupData.component.versionControlInformation);
+    };
+
+    /**
      * Determines whether the current selection could have provenance.
      *
      * @param {selection} selection
@@ -548,6 +631,16 @@
         {id: 'show-details-menu-item', condition: hasDetails, menuItem: {clazz: 'fa fa-gear', text: 'View configuration', action: 'showDetails'}},
         {id: 'variable-registry-menu-item', condition: hasVariables, menuItem: {clazz: 'fa', text: 'Variables', action: 'openVariableRegistry'}},
         {separator: true},
+        {id: 'version-menu-item', groupMenuItem: {clazz: 'fa', text: 'Version'}, menuItems: [
+            {id: 'start-version-control-menu-item', condition: supportsStartFlowVersioning, menuItem: {clazz: 'fa fa-floppy-o', text: 'Start version control', action: 'saveFlowVersion'}},
+            {separator: true},
+            {id: 'commit-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-floppy-o', text: 'Commit local changes', action: 'saveFlowVersion'}},
+            {id: 'revert-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Revert local changes', action: 'revertFlowChanges'}},
+            {id: 'change-version-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Change version', action: 'changeFlowVersion'}},
+            {separator: true},
+            {id: 'stop-version-control-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Stop version control', action: 'disconnectFlowVersioning'}}
+        ]},
+        {separator: true},
         {id: 'enter-group-menu-item', condition: isProcessGroup, menuItem: {clazz: 'fa fa-sign-in', text: 'Enter group', action: 'enterGroup'}},
         {separator: true},
         {id: 'start-menu-item', condition: isRunnable, menuItem: {clazz: 'fa fa-play', text: 'Start', action: 'start'}},

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
new file mode 100644
index 0000000..6315d30
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -0,0 +1,476 @@
+/*
+ * 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.
+ */
+
+/* global define, module, require, exports */
+
+/**
+ * Handles versioning.
+ */
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'nf.ng.Bridge',
+                'nf.ErrorHandler',
+                'nf.Dialog',
+                'nf.Common',
+                'nf.Client',
+                'nf.CanvasUtils',
+                'nf.ProcessGroup',
+                'nf.Graph'],
+            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph) {
+                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.FlowVerison =
+            factory(require('jquery'),
+                require('nf.ng.Bridge'),
+                require('nf.ErrorHandler'),
+                require('nf.Dialog'),
+                require('nf.Common'),
+                require('nf.Client'),
+                require('nf.CanvasUtils'),
+                require('nf.ProcessGroup'),
+                require('nf.Graph')));
+    } else {
+        nf.FlowVersion = factory(root.$,
+            root.nf.ng.Bridge,
+            root.nf.ErrorHandler,
+            root.nf.Dialog,
+            root.nf.Common,
+            root.nf.Client,
+            root.nf.CanvasUtils,
+            root.nf.ProcessGroup,
+            root.nf.Graph);
+    }
+}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph) {
+    'use strict';
+
+    /**
+     * Reset the dialog.
+     */
+    var resetDialog = function () {
+        $('#flow-version-registry-combo').combo('destroy').hide();
+        $('#flow-version-bucket-combo').combo('destroy').hide();
+
+        $('#flow-version-registry').text('').hide();
+        $('#flow-version-bucket').text('').hide();
+
+        $('#flow-version-name').val('');
+        $('#flow-version-description').val('');
+        $('#flow-version-change-comments').val('');
+
+        $('#flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+    };
+
+    /**
+     * Loads the buckets for the specified registryIdentifier for the current user.
+     *
+     * @param registryIdentifier
+     * @returns {*}
+     */
+    var loadBuckets = function (registryIdentifier) {
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets',
+            dataType: 'json'
+        }).done(function (response) {
+            var buckets = [];
+
+            if (nfCommon.isDefinedAndNotNull(response.buckets) && response.buckets.length > 0) {
+                response.buckets.sort(function (a, b) {
+                    return a.bucket.name > b.bucket.name;
+                });
+
+                $.each(response.buckets, function (_, bucketEntity) {
+                    var bucket = bucketEntity.bucket;
+                    buckets.push({
+                        text: bucket.name,
+                        value: bucket.id,
+                        description: nfCommon.escapeHtml(bucket.description)
+                    });
+                });
+            } else {
+                buckets.push({
+                    text: 'No available buckets',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                });
+            }
+
+            // load the buckets
+            $('#flow-version-bucket-combo').combo('destroy').combo({
+                options: buckets,
+                select: selectBucket
+            });
+        }).fail(function () {
+            $('#save-flow-version-dialog').modal('refreshButtons');
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Select handler for the registries combo.
+     *
+     * @param selectedOption
+     */
+    var selectRegistry = function (selectedOption) {
+        if (selectedOption.disabled === true) {
+            $('#flow-version-bucket-combo').combo('destroy').combo({
+                options: [{
+                    text: 'No available buckets',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
+
+            $('#save-flow-version-dialog').modal('refreshButtons');
+        } else {
+            loadBuckets(selectedOption.value);
+        }
+    };
+
+    /**
+     * Select handler for the buckets combo.
+     *
+     * @param selectedOption
+     */
+    var selectBucket = function (selectedOption) {
+        $('#save-flow-version-dialog').modal('refreshButtons');
+    };
+
+    /**
+     * Saves a flow version.
+     *
+     * @returns {*}
+     */
+    var saveFlowVersion = function () {
+        var processGroupId = $('#flow-version-process-group-id').text();
+        var processGroupRevision = $('#flow-version-process-group-id').data('revision');
+
+        var saveFlowVersionRequest = {
+            processGroupRevision: nfClient.getRevision({
+                revision: {
+                    version: processGroupRevision.version
+                }
+            })
+        };
+
+        var versionControlInformation = $('#flow-version-process-group-id').data('versionControlInformation');
+        if (nfCommon.isDefinedAndNotNull(versionControlInformation)) {
+            saveFlowVersionRequest['versionedFlow'] = {
+                registryId: versionControlInformation.registryId,
+                bucketId: versionControlInformation.bucketId,
+                flowId: versionControlInformation.flowId,
+                flowName: $('#flow-version-name').val(),
+                description: $('#flow-version-description').val(),
+                comments: $('#flow-version-change-comments').val()
+            }
+        } else {
+            var selectedRegistry =  $('#flow-version-registry-combo').combo('getSelectedOption');
+            var selectedBucket =  $('#flow-version-bucket-combo').combo('getSelectedOption');
+
+            saveFlowVersionRequest['versionedFlow'] = {
+                registryId: selectedRegistry.value,
+                bucketId: selectedBucket.value,
+                flowName: $('#flow-version-name').val(),
+                description: $('#flow-version-description').val(),
+                comments: $('#flow-version-change-comments').val()
+            }
+        }
+
+        return $.ajax({
+            type: 'POST',
+            data: JSON.stringify(saveFlowVersionRequest),
+            url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    return {
+        init: function () {
+            // initialize the flow version dialog
+            $('#save-flow-version-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Save Flow Version',
+                buttons: [{
+                    buttonText: 'Save',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    disabled: function () {
+                        if ($('#flow-version-registry-combo').is(':visible')) {
+                            var selectedRegistry =  $('#flow-version-registry-combo').combo('getSelectedOption');
+                            var selectedBucket =  $('#flow-version-bucket-combo').combo('getSelectedOption');
+
+                            if (nfCommon.isDefinedAndNotNull(selectedRegistry) && nfCommon.isDefinedAndNotNull(selectedBucket)) {
+                                return selectedRegistry.disabled === true || selectedBucket.disabled === true;
+                            } else {
+                                return true;
+                            }
+                        } else {
+                            return false;
+                        }
+                    },
+                    handler: {
+                        click: function () {
+                            var processGroupId = $('#flow-version-process-group-id').text();
+                            
+                            saveFlowVersion().done(function (response) {
+                                // refresh either selected PG or bread crumb to reflect connected/tracking status
+                                if (nfCanvasUtils.getGroupId() === processGroupId) {
+                                    nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, response.versionControlInformation);
+                                    nfNgBridge.digest();
+                                } else {
+                                    nfProcessGroup.reload(processGroupId);
+                                }
+
+                                // close the dialog
+                                $('#save-flow-version-dialog').modal('hide');
+                            });
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    color: {
+                        base: '#E3E8EB',
+                        hover: '#C7D2D7',
+                        text: '#004849'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+                handler: {
+                    close: function () {
+                        resetDialog();
+                    }
+                }
+            });
+        },
+
+        /**
+         * Shows the flow version dialog.
+         *
+         * @param processGroupId
+         */
+        showFlowVersionDialog: function (processGroupId) {
+            return $.Deferred(function (deferred) {
+                $.ajax({
+                    type: 'GET',
+                    url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                    dataType: 'json'
+                }).done(function (response) {
+                    // record the revision
+                    $('#flow-version-process-group-id').data('revision', response.processGroupRevision).text(processGroupId);
+
+                    if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                        var versionControlInformation = response.versionControlInformation;
+
+                        // update the registry and bucket visibility
+                        $('#flow-version-registry').text(versionControlInformation.registryId).show();
+                        $('#flow-version-bucket').text(versionControlInformation.bucketId).show();
+
+                        $('#flow-version-name').val('');
+                        $('#flow-version-description').val('');
+
+                        // record the versionControlInformation
+                        $('#flow-version-process-group-id').data('versionControlInformation', versionControlInformation)
+
+                        deferred.resolve();
+                    } else {
+                        // update the registry and bucket visibility
+                        $('#flow-version-registry-combo').show();
+                        $('#flow-version-bucket-combo').show();
+
+                        $.ajax({
+                            type: 'GET',
+                            url: '../nifi-api/flow/registries',
+                            dataType: 'json'
+                        }).done(function (registriesResponse) {
+                            var registries = [];
+
+                            if (nfCommon.isDefinedAndNotNull(registriesResponse.registries) && registriesResponse.registries.length > 0) {
+                                registriesResponse.registries.sort(function (a, b) {
+                                    return a.component.name > b.component.name;
+                                });
+
+                                $.each(registriesResponse.registries, function (_, registryEntity) {
+                                    var registry = registryEntity.component;
+                                    registries.push({
+                                        text: registry.name,
+                                        value: registry.id,
+                                        description: nfCommon.escapeHtml(registry.description)
+                                    });
+                                });
+                            } else {
+                                registries.push({
+                                    text: 'No available registries',
+                                    value: null,
+                                    optionClass: 'unset',
+                                    disabled: true
+                                });
+                            }
+
+                            // load the registries
+                            $('#flow-version-registry-combo').combo({
+                                options: registries,
+                                select: selectRegistry
+                            });
+
+                            deferred.resolve();
+                        }).fail(function () {
+                            deferred.reject();
+                        }).fail(nfErrorHandler.handleAjaxError);
+                    }
+                }).fail(nfErrorHandler.handleAjaxError);
+            }).done(function () {
+                $('#save-flow-version-dialog').modal('show');
+            }).fail(function () {
+                $('#save-flow-version-dialog').modal('refreshButtons');
+            }).promise();
+        },
+
+        /**
+         * Reverts changes for the specified Process Group.
+         *
+         * @param processGroupId
+         */
+        revertFlowChanges: function (processGroupId) {
+            // prompt the user before reverting
+            nfDialog.showYesNoDialog({
+                headerText: 'Revert Changes',
+                dialogContent: 'Are you sure you want to revert changes? All flow configuration changes will be reverted to the last version.',
+                noText: 'Cancel',
+                yesText: 'Revert',
+                yesHandler: function () {
+                    $.ajax({
+                        type: 'GET',
+                        url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                        dataType: 'json'
+                    }).done(function (response) {
+                        if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                            var revertFlowVersionRequest = {
+                                processGroupRevision: nfClient.getRevision({
+                                    revision: {
+                                        version: response.processGroupRevision.version
+                                    }
+                                }),
+                                versionControlInformation: response.versionControlInformation
+                            };
+
+                            $.ajax({
+                                type: 'POST',
+                                data: JSON.stringify(revertFlowVersionRequest),
+                                url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
+                                dataType: 'json',
+                                contentType: 'application/json'
+                            }).done(function (response) {
+                                // TODO update multi step to show user the ramifications of reverting for confirmation
+
+                                if (nfCanvasUtils.getGroupId() === processGroupId) {
+                                    // if reverting current PG... reload/refresh this group/canvas
+                                    // TODO consider implementing this differently
+                                    $.ajax({
+                                        type: 'GET',
+                                        url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId),
+                                        dataType: 'json'
+                                    }).done(function (response) {
+                                        nfGraph.set(response.processGroupFlow.flow);
+                                    }).fail(nfErrorHandler.handleAjaxError);
+                                } else {
+                                    // if reverting selected PG... reload selected PG to update counts, etc
+                                    nfProcessGroup.reload(processGroupId);
+                                }
+
+                                nfDialog.showOkDialog({
+                                    headerText: 'Revert Changes',
+                                    dialogContent: 'This Process Group has been reverted.'
+                                });
+                            }).fail(nfErrorHandler.handleAjaxError);
+                        } else {
+                            nfDialog.showOkDialog({
+                                headerText: 'Revert Changes',
+                                dialogContent: 'This Process Group is not currently under version control.'
+                            });
+                        }
+                    }).fail(nfErrorHandler.handleAjaxError);
+                }
+            });
+        },
+
+        /**
+         * Disconnects the specified Process Group from flow versioning.
+         *
+         * @param processGroupId
+         */
+        disconnectFlowVersioning: function (processGroupId) {
+            // prompt the user before disconnecting
+            nfDialog.showYesNoDialog({
+                headerText: 'Disconnect',
+                dialogContent: 'Are you sure you want to disconnect?',
+                noText: 'Cancel',
+                yesText: 'Disconnect',
+                yesHandler: function () {
+                    $.ajax({
+                        type: 'GET',
+                        url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                        dataType: 'json'
+                    }).done(function (response) {
+                        if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                            var revision = nfClient.getRevision({
+                                revision: {
+                                    version: response.processGroupRevision.version
+                                }
+                            });
+
+                            $.ajax({
+                                type: 'DELETE',
+                                url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId) + '?' + $.param(revision),
+                                dataType: 'json',
+                                contentType: 'application/json'
+                            }).done(function (response) {
+                                // refresh either selected PG or bread crumb to reflect disconnected status
+                                if (nfCanvasUtils.getGroupId() === processGroupId) {
+                                    nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, undefined);
+                                    nfNgBridge.digest();
+                                } else {
+                                    nfProcessGroup.reload(processGroupId);
+                                }
+
+                                nfDialog.showOkDialog({
+                                    headerText: 'Disconnect',
+                                    dialogContent: 'This Process Group has been disconnected.'
+                                });
+                            }).fail(nfErrorHandler.handleAjaxError);
+                        } else {
+                            nfDialog.showOkDialog({
+                                headerText: 'Disconnect',
+                                dialogContent: 'This Process Group is not currently under version control.'
+                            })
+                        }
+                    }).fail(nfErrorHandler.handleAjaxError);
+                }
+            });
+        }
+    };
+}));
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
index 498fbd7..58402df 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
@@ -215,7 +215,7 @@
                         $('#process-group-configuration').data('process-group', {
                             'permissions': {
                                 canRead: false,
-                                canWrite: nfCanvasUtils.canWrite()
+                                canWrite: nfCanvasUtils.canWriteCurrentGroup()
                             }
                         });
                     } else {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 13bbc70..17bb069 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -180,6 +180,15 @@
                 'class': 'process-group-name'
             });
 
+        // process group name
+        processGroup.append('text')
+            .attr({
+                'x': 3,
+                'y': 17,
+                'class': 'version-control'
+            })
+            .text('\uf00c');
+
         // always support selecting and navigation
         processGroup.on('dblclick', function (d) {
             // enter this group on double click
@@ -868,12 +877,18 @@
                         }).append('title').text(function (d) {
                         return d.component.name;
                     });
+
+                    // update version control information
+                    processGroup.select('text.version-control').style('visibility', nfCommon.isDefinedAndNotNull(processGroupData.component.versionControlInformation) ? 'visible' : 'hidden');
                 } else {
                     // clear the process group comments
                     details.select('text.process-group-comments').text(null);
 
                     // clear the process group name
                     processGroup.select('text.process-group-name').text(null);
+
+                    // update version control information
+                    processGroup.select('text.version-control').style('visibility', false);
                 }
 
                 // populate the stats

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
index 4da6fc6..0e5a2d7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
@@ -81,7 +81,9 @@
             controllerConfig: '../nifi-api/controller/config',
             reportingTaskTypes: '../nifi-api/flow/reporting-task-types',
             createReportingTask: '../nifi-api/controller/reporting-tasks',
-            reportingTasks: '../nifi-api/flow/reporting-tasks'
+            reportingTasks: '../nifi-api/flow/reporting-tasks',
+            createRegistry: '../nifi-api/controller/registries',
+            registries: '../nifi-api/flow/registries'
         }
     };
 
@@ -465,6 +467,94 @@
     };
 
     /**
+     * Adds the specified entity.
+     */
+    var addRegistry = function () {
+        var registryEntity = {
+            'revision': nfClient.getRevision({
+                'revision': {
+                    'version': 0
+                }
+            }),
+            'component': {
+                'name': $('#registry-name').val(),
+                'uri': $('#registry-location').val(),
+                'description': $('#registry-description').val()
+            }
+        };
+
+        // add the new registry
+        var addRegistry = $.ajax({
+            type: 'POST',
+            url: config.urls.createRegistry,
+            data: JSON.stringify(registryEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (registryEntity) {
+            // add the item
+            var registriesGrid = $('#registries-table').data('gridInstance');
+            var registriesData = registriesGrid.getData();
+            registriesData.addItem($.extend({
+                type: 'Registry'
+            }, registryEntity));
+
+            // resort
+            registriesData.reSort();
+            registriesGrid.invalidate();
+
+            // select the new reporting task
+            var row = registriesData.getRowById(registryEntity.id);
+            nfFilteredDialogCommon.choseRow(registriesGrid, row);
+            registriesGrid.scrollRowIntoView(row);
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#registry-configuration-dialog').modal('hide');
+
+        return addRegistry;
+    };
+
+    /**
+     * Updates the registry with the specified id.
+     *
+     * @param registryId
+     */
+    var updateRegistry = function (registryId) {
+        var registriesGrid = $('#registries-table').data('gridInstance');
+        var registriesData = registriesGrid.getData();
+
+        var registryEntity = registriesData.getItemById(registryId);
+        var requestRegistryEntity = {
+            'revision': nfClient.getRevision(registryEntity),
+            'component': {
+                'id': registryId,
+                'name': $('#registry-name').val(),
+                'uri': $('#registry-location').val(),
+                'description': $('#registry-description').val()
+            }
+        };
+
+        // add the new reporting task
+        var updateRegistry = $.ajax({
+            type: 'PUT',
+            url: registryEntity.uri,
+            data: JSON.stringify(requestRegistryEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (registryEntity) {
+            // add the item
+            registriesData.updateItem(registryId, $.extend({
+                type: 'Registry'
+            }, registryEntity));
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#registry-configuration-dialog').modal('hide');
+
+        return updateRegistry;
+    };
+
+    /**
      * Initializes the new reporting task dialog.
      */
     var initNewReportingTaskDialog = function () {
@@ -783,6 +873,20 @@
                 }
             }
         });
+
+        // initialize the registry configuration dialog
+        $('#registry-configuration-dialog').modal({
+            scrollableContentStyle: 'scrollable',
+            headerText: 'Add Registry',
+            handler: {
+                close: function () {
+                    $('#registry-id').text('');
+                    $('#registry-name').val('');
+                    $('#registry-location').val('');
+                    $('#registry-description').val('');
+                }
+            }
+        });
     };
 
     /**
@@ -1090,6 +1194,228 @@
         });
     };
 
+    var initRegistriesTable = function () {
+
+        var locationFormatter = function (row, cell, value, columnDef, dataContext) {
+            if (!dataContext.permissions.canRead) {
+                return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
+            }
+
+            return nfCommon.escapeHtml(dataContext.component.uri);
+        };
+
+        var descriptionFormatter = function (row, cell, value, columnDef, dataContext) {
+            if (!dataContext.permissions.canRead) {
+                return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
+            }
+
+            return nfCommon.escapeHtml(dataContext.component.description);
+        };
+
+        var registriesActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            if (nfCommon.canModifyController()) {
+                // edit registry
+                markup += '<div title="Edit" class="pointer edit-registry fa fa-pencil" style="margin-top: 2px; margin-right: 3px;" ></div>';
+
+                // remove registry
+                markup += '<div title="Remove" class="pointer remove-registry fa fa-trash" style="margin-top: 2px; margin-right: 3px;" ></div>';
+            }
+
+            return markup;
+        };
+
+        // define the column model for the reporting tasks table
+        var registriesColumnModel = [
+            {
+                id: 'name',
+                name: 'Name',
+                field: 'name',
+                formatter: nameFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'uri',
+                name: 'Location',
+                field: 'uri',
+                formatter: locationFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'description',
+                name: 'Description',
+                field: 'description',
+                formatter: descriptionFormatter,
+                sortable: true,
+                resizable: true
+            }
+        ];
+
+        // action column should always be last
+        registriesColumnModel.push({
+            id: 'actions',
+            name: '&nbsp;',
+            resizable: false,
+            formatter: registriesActionFormatter,
+            sortable: false,
+            width: 90,
+            maxWidth: 90
+        });
+
+        // initialize the dataview
+        var registriesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        registriesData.setItems([]);
+
+        // initialize the sort
+        sort({
+            columnId: 'name',
+            sortAsc: true
+        }, registriesData);
+
+        // initialize the grid
+        var registriesGrid = new Slick.Grid('#registries-table', registriesData, registriesColumnModel, gridOptions);
+        registriesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        registriesGrid.registerPlugin(new Slick.AutoTooltips());
+        registriesGrid.setSortColumn('name', true);
+        registriesGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.id,
+                sortAsc: args.sortAsc
+            }, registriesData);
+        });
+
+        // configure a click listener
+        registriesGrid.onClick.subscribe(function (e, args) {
+            var target = $(e.target);
+
+            // get the service at this row
+            var registryEntity = registriesData.getItem(args.row);
+
+            // determine the desired action
+            if (registriesGrid.getColumns()[args.cell].id === 'actions') {
+                if (target.hasClass('edit-registry')) {
+                    editRegistry(registryEntity);
+                } else if (target.hasClass('remove-registry')) {
+                    promptToRemoveRegistry(registryEntity);
+                }
+            } else if (registriesGrid.getColumns()[args.cell].id === 'moreDetails') {
+                // if (target.hasClass('view-reporting-task')) {
+                //     nfReportingTask.showDetails(reportingTaskEntity);
+                // } else if (target.hasClass('reporting-task-usage')) {
+                //     // close the settings dialog
+                //     $('#shell-close-button').click();
+                //
+                //     // open the documentation for this reporting task
+                //     nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                //         select: reportingTaskEntity.component.type,
+                //         group: reportingTaskEntity.component.bundle.group,
+                //         artifact: reportingTaskEntity.component.bundle.artifact,
+                //         version: reportingTaskEntity.component.bundle.version
+                //     })).done(function () {
+                //         nfSettings.showSettings();
+                //     });
+                // }
+            }
+        });
+
+        // wire up the dataview to the grid
+        registriesData.onRowCountChanged.subscribe(function (e, args) {
+            registriesGrid.updateRowCount();
+            registriesGrid.render();
+        });
+        registriesData.onRowsChanged.subscribe(function (e, args) {
+            registriesGrid.invalidateRows(args.rows);
+            registriesGrid.render();
+        });
+        registriesData.syncGridSelection(registriesGrid, true);
+
+        // hold onto an instance of the grid
+        $('#registries-table').data('gridInstance', registriesGrid);
+    };
+
+    /**
+     * Edits the specified registry entity.
+     *
+     * @param registryEntity
+     */
+    var editRegistry = function (registryEntity) {
+        // populate the dialog
+        $('#registry-id').text(registryEntity.id);
+        $('#registry-name').val(registryEntity.component.name);
+        $('#registry-location').val(registryEntity.component.uri);
+        $('#registry-description').val(registryEntity.component.description);
+
+        // show the dialog
+        $('#registry-configuration-dialog').modal('setButtonModel', [{
+            buttonText: 'Update',
+            color: {
+                base: '#728E9B',
+                hover: '#004849',
+                text: '#ffffff'
+            },
+            handler: {
+                click: function () {
+                    updateRegistry(registryEntity.id);
+                }
+            }
+        }, {
+            buttonText: 'Cancel',
+            color: {
+                base: '#E3E8EB',
+                hover: '#C7D2D7',
+                text: '#004849'
+            },
+            handler: {
+                click: function () {
+                    $(this).modal('hide');
+                }
+            }
+        }]).modal('show');
+    };
+
+    /**
+     * Prompts the user before attempting to delete the specified registry.
+     *
+     * @param {object} registryEntity
+     */
+    var promptToRemoveRegistry = function (registryEntity) {
+        // prompt for deletion
+        nfDialog.showYesNoDialog({
+            headerText: 'Delete Registry',
+            dialogContent: 'Delete registry \'' + nfCommon.escapeHtml(registryEntity.component.name) + '\'?',
+            yesHandler: function () {
+                removeRegistry(registryEntity);
+            }
+        });
+    };
+
+    /**
+     * Deletes the specified registry.
+     *
+     * @param {object} registryEntity
+     */
+    var removeRegistry = function (registryEntity) {
+        var revision = nfClient.getRevision(registryEntity);
+        $.ajax({
+            type: 'DELETE',
+            url: registryEntity.uri + '?' + $.param({
+                version: revision.version,
+                clientId: revision.clientId
+            }),
+            dataType: 'json'
+        }).done(function (response) {
+            // remove the task
+            var registryGrid = $('#registries-table').data('gridInstance');
+            var registryData = registryGrid.getData();
+            registryData.deleteItem(registryEntity.id);
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
     /**
      * Loads the settings.
      */
@@ -1158,6 +1484,9 @@
         // load the reporting tasks
         var reportingTasks = loadReportingTasks();
 
+        // load the registries
+        var registries = loadRegistries();
+
         // return a deferred for all parts of the settings
         return $.when(settings, controllerServicesXhr, reportingTasks).done(function (settingsResult, controllerServicesResult) {
             var controllerServicesResponse = controllerServicesResult[0];
@@ -1199,6 +1528,32 @@
     };
 
     /**
+     * Loads the registries.
+     */
+    var loadRegistries = function () {
+        return $.ajax({
+            type: 'GET',
+            url: config.urls.registries,
+            dataType: 'json'
+        }).done(function (response) {
+            var registries = [];
+            $.each(response.registries, function (_, registryEntity) {
+                registries.push($.extend({
+                    type: 'Registry'
+                }, registryEntity));
+            });
+
+            var registriesGrid = $('#registries-table').data('gridInstance');
+            var registriesData = registriesGrid.getData();
+
+            // update the registries
+            registriesData.setItems(registries);
+            registriesData.reSort();
+            registriesGrid.invalidate();
+        });
+    };
+
+    /**
      * Shows the process group configuration.
      */
     var showSettings = function () {
@@ -1241,6 +1596,9 @@
                 }, {
                     name: 'Reporting Tasks',
                     tabContentId: 'reporting-tasks-tab-content'
+                }, {
+                    name: 'Registries',
+                    tabContentId: 'registries-tab-content'
                 }],
                 select: function () {
                     var tab = $(this).text();
@@ -1267,6 +1625,9 @@
                                 } else if (tab === 'Reporting Tasks') {
                                     $('#settings-save').hide();
                                     return 'Create a new reporting task';
+                                } else if (tab === 'Registries') {
+                                    $('#settings-save').hide();
+                                    return 'Register a new registry';
                                 }
                             });
                         } else {
@@ -1276,7 +1637,7 @@
 
                         if (tab === 'Reporting Task Controller Services') {
                             $('#controller-cs-availability').show();
-                        } else if (tab === 'Reporting Tasks') {
+                        } else if (tab === 'Reporting Tasks' || tab === 'Registries') {
                             $('#controller-cs-availability').hide();
                         }
 
@@ -1315,6 +1676,32 @@
 
                     // set the initial focus
                     $('#reporting-task-type-filter').focus();
+                } else if (selectedTab === 'Registries') {
+                    $('#registry-configuration-dialog').modal('setButtonModel', [{
+                        buttonText: 'Add',
+                        color: {
+                            base: '#728E9B',
+                            hover: '#004849',
+                            text: '#ffffff'
+                        },
+                        handler: {
+                            click: function () {
+                                addRegistry();
+                            }
+                        }
+                    }, {
+                        buttonText: 'Cancel',
+                        color: {
+                            base: '#E3E8EB',
+                            hover: '#C7D2D7',
+                            text: '#004849'
+                        },
+                        handler: {
+                            click: function () {
+                                $(this).modal('hide');
+                            }
+                        }
+                    }]).modal('show');
                 }
             });
 
@@ -1322,6 +1709,7 @@
             initGeneral();
             nfControllerServices.init(getControllerServicesTable(), nfSettings.showSettings);
             initReportingTasks();
+            initRegistriesTable();
         },
 
         /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
index 10d36f1..a8b8579 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
@@ -22,6 +22,7 @@ limitations under the License.
                 <span ng-if="separatorFunc(crumb.parentBreadcrumb)" style="margin: 0 12px;">
                     &raquo;
                 </span>
+                <span ng-if="separatorFunc(crumb.breadcrumb.versionControlInformation)" class="breadcrumb-version-control fa fa-check" style="margin: 0 6px;"></span>
                 <span class="link"
                       ng-class="(highlightCrumbId === crumb.id) ? 'link-bold' : ''"
                       ng-click="clickFunc(crumb.id)">


[03/50] nifi git commit: NIFI-4436: Added additional endpoints; bug fixes

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
new file mode 100644
index 0000000..eb2ac76
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
@@ -0,0 +1,66 @@
+/*
+ * 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.web.dao.impl;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.web.ResourceNotFoundException;
+import org.apache.nifi.web.api.dto.RegistryDTO;
+import org.apache.nifi.web.dao.RegistryDAO;
+
+public class FlowRegistryDAO implements RegistryDAO {
+    private FlowRegistryClient flowRegistryClient;
+
+    @Override
+    public FlowRegistry createFlowRegistry(final RegistryDTO registryDto) {
+        return flowRegistryClient.addFlowRegistry(registryDto.getId(), registryDto.getName(), registryDto.getUri(), registryDto.getDescription());
+    }
+
+    @Override
+    public FlowRegistry getFlowRegistry(final String registryId) {
+        final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
+        if (registry == null) {
+            throw new ResourceNotFoundException("Unable to find Flow Registry with id '" + registryId + "'");
+        }
+
+        return registry;
+    }
+
+    @Override
+    public Set<FlowRegistry> getFlowRegistries() {
+        return flowRegistryClient.getRegistryIdentifiers().stream()
+            .map(flowRegistryClient::getFlowRegistry)
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public FlowRegistry removeFlowRegistry(final String registryId) {
+        final FlowRegistry registry = flowRegistryClient.removeFlowRegistry(registryId);
+        if (registry == null) {
+            throw new ResourceNotFoundException("Unable to find Flow Registry with id '" + registryId + "'");
+        }
+        return registry;
+    }
+
+    public void setFlowRegistryClient(FlowRegistryClient client) {
+        this.flowRegistryClient = client;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index f842a8c..7828337 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -246,6 +246,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         return group;
     }
 
+    @Override
     public ProcessGroup disconnectVersionControl(final String groupId) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
         group.disconnectVersionControl();
@@ -254,9 +255,9 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
 
     @Override
     public ProcessGroup updateProcessGroupFlow(final String groupId, final VersionedFlowSnapshot proposedSnapshot, final VersionControlInformationDTO versionControlInformation,
-        final String componentIdSeed, final boolean verifyNotModified) {
+        final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
-        group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified);
+        group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified, updateSettings);
 
         final StandardVersionControlInformation svci = new StandardVersionControlInformation(
             versionControlInformation.getRegistryId(),

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 99a4f1c..e71de67 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -125,6 +125,9 @@
         <property name="flowController" ref="flowController"/>
         <property name="snippetUtils" ref="snippetUtils"/>
     </bean>
+    <bean id="flowRegistryDAO" class="org.apache.nifi.web.dao.impl.FlowRegistryDAO">
+        <property name="flowRegistryClient" ref="flowRegistryClient" />
+    </bean>
     <bean id="policyBasedAuthorizerDAO" class="org.apache.nifi.web.dao.impl.StandardPolicyBasedAuthorizerDAO">
         <constructor-arg ref="authorizer"/>
     </bean>
@@ -182,6 +185,7 @@
         <property name="bulletinRepository" ref="bulletinRepository"/>
         <property name="leaderElectionManager" ref="leaderElectionManager" />
         <property name="flowRegistryClient" ref="flowRegistryClient" />
+        <property name="registryDAO" ref="flowRegistryDAO" />
     </bean>
     
     <!-- component ui extension configuration context -->


[28/50] nifi git commit: NIFI-4436: More intelligently flag a ProcessGroup to indicate whether or not it has any local modifications compared to Versioned Flow - Bug fixes - Updated to include status of a Versioned Process Group to include VersionedFlowS

Posted by bb...@apache.org.
NIFI-4436: More intelligently flag a ProcessGroup to indicate whether or not it has any local modifications compared to Versioned Flow - Bug fixes - Updated to include status of a Versioned Process Group to include VersionedFlowState and explanation

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/fdef5b56
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/fdef5b56
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/fdef5b56

Branch: refs/heads/master
Commit: fdef5b560544a8da33068b4acb6d4404fe193ed9
Parents: d34fb5e
Author: Mark Payne <ma...@hotmail.com>
Authored: Tue Nov 28 12:33:00 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:54 2018 -0500

----------------------------------------------------------------------
 .../api/dto/VersionControlInformationDTO.java   |  22 ++
 .../service/ControllerServiceNode.java          |   3 +-
 .../org/apache/nifi/groups/ProcessGroup.java    |  13 +-
 .../apache/nifi/groups/RemoteProcessGroup.java  |   9 +-
 .../flow/VersionControlInformation.java         |   5 +
 .../nifi/registry/flow/VersionedFlowState.java  |  52 +++
 .../nifi/registry/flow/VersionedFlowStatus.java |  31 ++
 .../apache/nifi/controller/FlowController.java  |  76 ++--
 .../controller/StandardFlowSynchronizer.java    |   5 +-
 .../nifi/groups/StandardProcessGroup.java       | 382 +++++++++++++++++--
 .../groups/StandardVersionedFlowStatus.java     |  50 +++
 .../flow/StandardVersionControlInformation.java |  17 +-
 .../flow/mapping/NiFiRegistryDtoMapper.java     | 328 ----------------
 .../flow/mapping/NiFiRegistryFlowMapper.java    |  97 +++--
 .../nifi/remote/StandardRemoteProcessGroup.java | 126 ++----
 .../service/mock/MockProcessGroup.java          |   6 +-
 .../nifi/web/StandardNiFiServiceFacade.java     |  31 +-
 .../nifi/web/api/ProcessGroupResource.java      |   2 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  29 +-
 .../nifi/web/controller/ControllerFacade.java   |   5 +
 .../dao/impl/StandardControllerServiceDAO.java  |  20 +-
 .../nifi/web/dao/impl/StandardFunnelDAO.java    |   2 +
 .../nifi/web/dao/impl/StandardInputPortDAO.java |   1 +
 .../nifi/web/dao/impl/StandardLabelDAO.java     |   1 +
 .../web/dao/impl/StandardOutputPortDAO.java     |   1 +
 .../web/dao/impl/StandardProcessGroupDAO.java   |  13 +-
 .../nifi/web/dao/impl/StandardProcessorDAO.java |   1 +
 .../dao/impl/StandardRemoteProcessGroupDAO.java |   5 +-
 .../nifi/web/util/AffectedComponentUtils.java   |   4 +
 .../ClusterReplicationComponentLifecycle.java   |   6 +-
 30 files changed, 751 insertions(+), 592 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
index c31a957..944b10a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
@@ -34,6 +34,8 @@ public class VersionControlInformationDTO {
     private Integer version;
     private Boolean modified;
     private Boolean current;
+    private String state;
+    private String stateExplanation;
 
     @ApiModelProperty("The ID of the Process Group that is under version control")
     public String getGroupId() {
@@ -135,4 +137,24 @@ public class VersionControlInformationDTO {
     public void setCurrent(Boolean current) {
         this.current = current;
     }
+
+    @ApiModelProperty(readOnly = true,
+        value = "The current state of the Process Group, as it relates to the Versioned Flow",
+        allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")
+    public String getState() {
+        return state;
+    }
+
+    public void setState(final String state) {
+        this.state = state;
+    }
+
+    @ApiModelProperty(readOnly = true, value = "Explanation of why the group is in the specified state")
+    public String getStateExplanation() {
+        return stateExplanation;
+    }
+
+    public void setStateExplanation(String explanation) {
+        this.stateExplanation = explanation;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
index 2f28963..2219d6d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.controller.service;
 
+import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.controller.ConfiguredComponent;
 import org.apache.nifi.controller.ControllerService;
@@ -27,7 +28,7 @@ import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
 
-public interface ControllerServiceNode extends ConfiguredComponent, VersionedComponent {
+public interface ControllerServiceNode extends ConfiguredComponent, ConfigurableComponent, VersionedComponent {
 
     /**
      * @return the Process Group that this Controller Service belongs to, or <code>null</code> if the Controller Service

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index d81b7d3..17131dd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -462,11 +462,11 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
 
     /**
      * @param id of the Controller Service
-     * @return the Controller Service with the given ID, if it exists as a child or
-     *         descendant of this ProcessGroup. This performs a recursive search of all
-     *         descendant ProcessGroups
+     * @param includeDescendantGroups whether or not to include descendant process groups
+     * @param includeAncestorGroups whether or not to include ancestor process groups
+     * @return the Controller Service with the given ID
      */
-    ControllerServiceNode findControllerService(String id);
+    ControllerServiceNode findControllerService(String id, boolean includeDescendantGroups, boolean includeAncestorGroups);
 
     /**
      * @return a List of all Controller Services contained within this ProcessGroup and any child Process Groups
@@ -976,4 +976,9 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
      * @param flowRegistry the Flow Registry to synchronize with
      */
     void synchronizeWithFlowRegistry(FlowRegistryClient flowRegistry);
+
+    /**
+     * Called whenever a component within this group or the group itself is modified
+     */
+    void onComponentModified();
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
index 0dd6070..7d92246 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
@@ -61,9 +61,9 @@ public interface RemoteProcessGroup extends ComponentAuthorizable, Positionable,
 
     void setName(String name);
 
-    void setInputPorts(Set<RemoteProcessGroupPortDescriptor> ports);
+    void setInputPorts(Set<RemoteProcessGroupPortDescriptor> ports, boolean pruneUnusedPorts);
 
-    void setOutputPorts(Set<RemoteProcessGroupPortDescriptor> ports);
+    void setOutputPorts(Set<RemoteProcessGroupPortDescriptor> ports, boolean pruneUnusedPorts);
 
     Set<RemoteGroupPort> getInputPorts();
 
@@ -216,11 +216,6 @@ public interface RemoteProcessGroup extends ComponentAuthorizable, Positionable,
     void reinitialize(boolean isClustered);
 
     /**
-     * Removes all non existent ports from this RemoteProcessGroup.
-     */
-    void removeAllNonExistentPorts();
-
-    /**
      * Removes a port that no longer exists on the remote instance from this
      * RemoteProcessGroup
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
index b54a1c9..1f65a19 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
@@ -77,6 +77,11 @@ public interface VersionControlInformation {
     boolean isCurrent();
 
     /**
+     * @return the current status of the Process Group as it relates to the associated Versioned Flow.
+     */
+    VersionedFlowStatus getStatus();
+
+    /**
      * @return the snapshot of the flow that was synchronized with the Flow Registry
      */
     VersionedProcessGroup getFlowSnapshot();

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
new file mode 100644
index 0000000..d20a13f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
@@ -0,0 +1,52 @@
+/*
+ * 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.registry.flow;
+
+public enum VersionedFlowState {
+
+    /**
+     * We are unable to communicate with the Flow Registry in order to determine the appropriate state
+     */
+    SYNC_FAILURE,
+
+    /**
+     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
+     * is on the latest version of the Versioned Flow, but is different than the Versioned Flow that is
+     * stored in the Flow Registry.
+     */
+    LOCALLY_MODIFIED,
+
+    /**
+     * This Process Group has not been modified since it was last synchronized with the Flow Registry, but
+     * the Flow Registry has a newer version of the flow than what is contained in this Process Group.
+     */
+    STALE,
+
+    /**
+     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
+     * has been modified since it was last synchronized with the Flow Registry, and the Flow Registry has
+     * a newer version of the flow than what is contained in this Process Group.
+     */
+    LOCALLY_MODIFIED_AND_STALE,
+
+    /**
+     * This Process Group and all child/descendant Process Groups are on the latest version of the flow in
+     * the Flow Registry and have no local modifications.
+     */
+    UP_TO_DATE;
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowStatus.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowStatus.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowStatus.java
new file mode 100644
index 0000000..9b58d9a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowStatus.java
@@ -0,0 +1,31 @@
+/*
+ * 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.registry.flow;
+
+public interface VersionedFlowStatus {
+
+    /**
+     * @return the current state of the versioned process group
+     */
+    VersionedFlowState getState();
+
+    /**
+     * @return an explanation of why the process group is in the state that it is in.
+     */
+    String getStateExplanation();
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 3909387..2afa9dc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -16,6 +16,39 @@
  */
 package org.apache.nifi.controller;
 
+import static java.util.Objects.requireNonNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+import javax.net.ssl.SSLContext;
+
 import org.apache.commons.collections4.Predicate;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -169,7 +202,6 @@ import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
-import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.registry.variable.StandardComponentVariableRegistry;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
@@ -225,38 +257,6 @@ import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.net.ssl.SSLContext;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
-
-import static java.util.Objects.requireNonNull;
-
 public class FlowController implements EventAccess, ControllerServiceProvider, ReportingTaskProvider,
     QueueProvider, Authorizable, ProvenanceAuthorizableFactory, NodeTypeProvider, IdentifierLookup, ReloadComponent {
 
@@ -1983,14 +1983,14 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 if (remoteGroupDTO.getContents() != null) {
                     final RemoteProcessGroupContentsDTO contents = remoteGroupDTO.getContents();
 
-                    // ensure there input ports
+                    // ensure there are input ports
                     if (contents.getInputPorts() != null) {
-                        remoteGroup.setInputPorts(convertRemotePort(contents.getInputPorts()));
+                        remoteGroup.setInputPorts(convertRemotePort(contents.getInputPorts()), false);
                     }
 
                     // ensure there are output ports
                     if (contents.getOutputPorts() != null) {
-                        remoteGroup.setOutputPorts(convertRemotePort(contents.getOutputPorts()));
+                        remoteGroup.setOutputPorts(convertRemotePort(contents.getOutputPorts()), false);
                     }
                 }
 
@@ -2035,12 +2035,8 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 instantiateSnippet(childGroup, childTemplateDTO, false);
 
                 if (groupDTO.getVersionControlInformation() != null) {
-                    final NiFiRegistryFlowMapper flowMapper = new NiFiRegistryFlowMapper();
-                    final VersionedProcessGroup versionedGroup = flowMapper.mapProcessGroup(childGroup, getFlowRegistryClient(), false);
-
                     final VersionControlInformation vci = StandardVersionControlInformation.Builder
                         .fromDto(groupDTO.getVersionControlInformation())
-                        .flowSnapshot(versionedGroup)
                         .build();
                     childGroup.setVersionControlInformation(vci, Collections.emptyMap());
                 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index 71a587c..28d9b79 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -931,6 +931,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
             final Label label = controller.createLabel(labelDTO.getId(), labelDTO.getLabel());
             label.setStyle(labelDTO.getStyle());
             label.setPosition(new Position(labelDTO.getPosition().getX(), labelDTO.getPosition().getY()));
+            label.setVersionedComponentId(labelDTO.getVersionedComponentId());
             if (labelDTO.getWidth() != null && labelDTO.getHeight() != null) {
                 label.setSize(new Size(labelDTO.getWidth(), labelDTO.getHeight()));
             }
@@ -1327,13 +1328,13 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
             for (final Element portElement : getChildrenByTagName(remoteProcessGroupElement, "inputPort")) {
                 inputPorts.add(FlowFromDOMFactory.getRemoteProcessGroupPort(portElement));
             }
-            remoteGroup.setInputPorts(inputPorts);
+            remoteGroup.setInputPorts(inputPorts, false);
 
             final Set<RemoteProcessGroupPortDescriptor> outputPorts = new HashSet<>();
             for (final Element portElement : getChildrenByTagName(remoteProcessGroupElement, "outputPort")) {
                 outputPorts.add(FlowFromDOMFactory.getRemoteProcessGroupPort(portElement));
             }
-            remoteGroup.setOutputPorts(outputPorts);
+            remoteGroup.setOutputPorts(outputPorts, false);
             processGroup.addRemoteProcessGroup(remoteGroup);
 
             for (final RemoteProcessGroupPortDescriptor remoteGroupPortDTO : outputPorts) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 9a14464..4b186a9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -83,11 +83,14 @@ import org.apache.nifi.registry.flow.VersionedControllerService;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowState;
+import org.apache.nifi.registry.flow.VersionedFlowStatus;
 import org.apache.nifi.registry.flow.VersionedFunnel;
 import org.apache.nifi.registry.flow.VersionedLabel;
 import org.apache.nifi.registry.flow.VersionedPort;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
 import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
 import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
@@ -166,6 +169,8 @@ public final class StandardProcessGroup implements ProcessGroup {
     private final Map<String, Template> templates = new HashMap<>();
     private final StringEncryptor encryptor;
     private final MutableVariableRegistry variableRegistry;
+    private final AtomicReference<StandardVersionedFlowStatus> flowStatus = new AtomicReference<>(
+        new StandardVersionedFlowStatus(null, "Not yet synchronized with Flow Registry", null));
 
     private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
     private final Lock readLock = rwLock.readLock();
@@ -494,6 +499,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             port.setProcessGroup(this);
             inputPorts.put(requireNonNull(port).getIdentifier(), port);
             flowController.onInputPortAdded(port);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -528,6 +534,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 throw new IllegalStateException(port.getIdentifier() + " is not an Input Port of this Process Group");
             }
 
+            onComponentModified();
+
             flowController.onInputPortRemoved(port);
             LOG.info("Input Port {} removed from flow", port);
         } finally {
@@ -575,6 +583,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             port.setProcessGroup(this);
             outputPorts.put(port.getIdentifier(), port);
             flowController.onOutputPortAdded(port);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -600,6 +609,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 throw new IllegalStateException(port.getIdentifier() + " is not an Output Port of this Process Group");
             }
 
+            onComponentModified();
+
             flowController.onOutputPortRemoved(port);
             LOG.info("Output Port {} removed from flow", port);
         } finally {
@@ -640,6 +651,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             processGroups.put(Objects.requireNonNull(group).getIdentifier(), group);
             flowController.onProcessGroupAdded(group);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -679,6 +691,8 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             removeComponents(group);
             processGroups.remove(group.getIdentifier());
+            onComponentModified();
+
             flowController.onProcessGroupRemoved(group);
             LOG.info("{} removed from flow", group);
         } finally {
@@ -734,6 +748,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             remoteGroup.setProcessGroup(this);
             remoteGroups.put(Objects.requireNonNull(remoteGroup).getIdentifier(), remoteGroup);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -767,6 +782,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 }
             }
 
+            onComponentModified();
+
             for (final RemoteGroupPort port : remoteGroup.getOutputPorts()) {
                 // must copy to avoid a concurrent modification
                 final Set<Connection> copy = new HashSet<>(port.getConnections());
@@ -802,6 +819,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             processor.getVariableRegistry().setParent(getVariableRegistry());
             processors.put(processorId, processor);
             flowController.onProcessorAdded(processor);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -843,6 +861,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
 
             processors.remove(id);
+            onComponentModified();
+
             flowController.onProcessorRemoved(processor);
             LogRepositoryFactory.getRepository(processor.getIdentifier()).removeAllObservers();
 
@@ -912,6 +932,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         writeLock.lock();
         try {
             connections.put(connection.getIdentifier(), connection);
+            onComponentModified();
             connection.setProcessGroup(this);
         } finally {
             writeLock.unlock();
@@ -983,6 +1004,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
             connections.put(connection.getIdentifier(), connection);
             flowController.onConnectionAdded(connection);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -1042,6 +1064,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             // remove the connection from our map
             connections.remove(connection.getIdentifier());
             LOG.info("{} removed from flow", connection);
+            onComponentModified();
+
             flowController.onConnectionRemoved(connection);
         } finally {
             writeLock.unlock();
@@ -1109,6 +1133,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             label.setProcessGroup(this);
             labels.put(label.getIdentifier(), label);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -1123,6 +1148,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 throw new IllegalStateException(label + " is not a member of this Process Group.");
             }
 
+            onComponentModified();
             LOG.info("Label with ID {} removed from flow", label.getIdentifier());
         } finally {
             writeLock.unlock();
@@ -1828,6 +1854,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             if (autoStart) {
                 startFunnel(funnel);
             }
+
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -1859,18 +1887,43 @@ public final class StandardProcessGroup implements ProcessGroup {
 
 
     @Override
-    public ControllerServiceNode findControllerService(final String id) {
-        return findControllerService(id, this);
+    public ControllerServiceNode findControllerService(final String id, final boolean includeDescendants, final boolean includeAncestors) {
+        ControllerServiceNode serviceNode;
+        if (includeDescendants) {
+            serviceNode = findDescendantControllerService(id, this);
+        } else {
+            serviceNode = getControllerService(id);
+        }
+
+        if (serviceNode == null && includeAncestors) {
+            serviceNode = findAncestorControllerService(id, getParent());
+        }
+
+        return serviceNode;
+    }
+
+    private ControllerServiceNode findAncestorControllerService(final String id, final ProcessGroup start) {
+        if (start == null) {
+            return null;
+        }
+
+        final ControllerServiceNode serviceNode = start.getControllerService(id);
+        if (serviceNode != null) {
+            return serviceNode;
+        }
+
+        final ProcessGroup parent = start.getParent();
+        return findAncestorControllerService(id, parent);
     }
 
-    private ControllerServiceNode findControllerService(final String id, final ProcessGroup start) {
+    private ControllerServiceNode findDescendantControllerService(final String id, final ProcessGroup start) {
         ControllerServiceNode service = start.getControllerService(id);
         if (service != null) {
             return service;
         }
 
         for (final ProcessGroup group : start.getProcessGroups()) {
-            service = findControllerService(id, group);
+            service = findDescendantControllerService(id, group);
             if (service != null) {
                 return service;
             }
@@ -1916,6 +1969,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
 
             funnels.remove(funnel.getIdentifier());
+            onComponentModified();
+
             flowController.onFunnelRemoved(funnel);
             LOG.info("{} removed from flow", funnel);
         } finally {
@@ -1947,6 +2002,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             service.getVariableRegistry().setParent(getVariableRegistry());
             this.controllerServices.put(service.getIdentifier(), service);
             LOG.info("{} added to {}", service, this);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -2010,6 +2066,21 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
 
             controllerServices.remove(service.getIdentifier());
+            onComponentModified();
+
+            // For any component that references this Controller Service, find the component's Process Group
+            // and notify the Process Group that a component has been modified. This way, we know to re-calculate
+            // whether or not the Process Group has local modifications.
+            service.getReferences().getReferencingComponents().stream()
+                .map(ConfiguredComponent::getProcessGroupIdentifier)
+                .filter(id -> !id.equals(getIdentifier()))
+                .forEach(groupId -> {
+                    final ProcessGroup descendant = findProcessGroup(groupId);
+                    if (descendant != null) {
+                        descendant.onComponentModified();
+                    }
+                });
+
             flowController.getStateManagerProvider().onComponentRemoved(service.getIdentifier());
 
             removed = true;
@@ -2043,6 +2114,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             templates.put(id, template);
             template.setProcessGroup(this);
             LOG.info("{} added to {}", template, this);
+            onComponentModified();
         } finally {
             writeLock.unlock();
         }
@@ -2112,6 +2184,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
 
             templates.remove(template.getIdentifier());
+            onComponentModified();
+
             LOG.info("{} removed from flow", template);
         } finally {
             writeLock.unlock();
@@ -2172,6 +2246,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 toRemove.verifyCanDelete(true);
             }
 
+            onComponentModified();
+
             for (final String id : connectionIdsToRemove) {
                 removeConnection(connections.get(id));
             }
@@ -2224,6 +2300,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 throw new IllegalStateException("Cannot move Ports into the root group");
             }
 
+            onComponentModified();
+
             for (final String id : getKeys(snippet.getInputPorts())) {
                 destination.addInputPort(inputPorts.remove(id));
             }
@@ -2845,6 +2923,34 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     @Override
+    public void onComponentModified() {
+        // We no longer know if or how the Process Group has changed, so the next time that we
+        // get the local modifications, we must re-calculate it. We cannot simply assume that
+        // the flow was modified now, because if a Processor Property changed from 'A' to 'B',
+        // then back to 'A', then we have to know that it was not modified. So we set it to null
+        // to indicate that we must calculate the local modifications.
+        final StandardVersionControlInformation svci = this.versionControlInfo.get();
+        if (svci == null) {
+            // This group is not under version control directly. Notify parent.
+            final ProcessGroup parentGroup = parent.get();
+            if (parentGroup != null) {
+                parentGroup.onComponentModified();
+            }
+        }
+
+        clearFlowDifferences();
+    }
+
+    private void clearFlowDifferences() {
+        boolean updated = false;
+        while (!updated) {
+            final StandardVersionedFlowStatus status = flowStatus.get();
+            final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(status.getState(), status.getStateExplanation(), null);
+            updated = flowStatus.compareAndSet(status, updatedStatus);
+        }
+    }
+
+    @Override
     public void setVersionControlInformation(final VersionControlInformation versionControlInformation, final Map<String, String> versionedComponentIds) {
         final StandardVersionControlInformation svci = new StandardVersionControlInformation(
             versionControlInformation.getRegistryIdentifier(),
@@ -2854,16 +2960,63 @@ public final class StandardProcessGroup implements ProcessGroup {
             versionControlInformation.getVersion(),
             stripContentsFromRemoteDescendantGroups(versionControlInformation.getFlowSnapshot(), true),
             versionControlInformation.isModified(),
-            versionControlInformation.isCurrent()) {
+            versionControlInformation.isCurrent(),
+            versionControlInformation.getStatus()) {
 
             @Override
             public boolean isModified() {
-                final Set<FlowDifference> differences = StandardProcessGroup.this.getModifications();
-                if (differences == null) {
-                    return false;
+                boolean updated = false;
+                while (true) {
+                    final StandardVersionedFlowStatus status = flowStatus.get();
+                    Set<FlowDifference> differences = status.getCurrentDifferences();
+                    if (differences == null) {
+                        differences = getModifications();
+                        if (differences == null) {
+                            return false;
+                        }
+
+                        final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(status.getState(), status.getStateExplanation(), differences);
+                        updated = flowStatus.compareAndSet(status, updatedStatus);
+
+                        if (updated) {
+                            return !differences.isEmpty();
+                        }
+
+                        continue;
+                    }
+
+                    return !differences.isEmpty();
+                }
+            }
+
+            @Override
+            public VersionedFlowStatus getStatus() {
+                // If current state is a sync failure, then
+                final StandardVersionedFlowStatus status = flowStatus.get();
+                final VersionedFlowState state = status.getState();
+                if (state == VersionedFlowState.SYNC_FAILURE) {
+                    return status;
                 }
 
-                return !differences.isEmpty();
+                final boolean modified = isModified();
+                if (!modified) {
+                    final VersionControlInformation vci = StandardProcessGroup.this.versionControlInfo.get();
+                    if (vci.getFlowSnapshot() == null) {
+                        return new StandardVersionedFlowStatus(VersionedFlowState.SYNC_FAILURE, "Process Group has not yet been synchronized with Flow Registry", null);
+                    }
+                }
+
+                final boolean stale = !isCurrent();
+
+                if (modified && stale) {
+                    return new StandardVersionedFlowStatus(VersionedFlowState.LOCALLY_MODIFIED_AND_STALE, "Local changes have been made and a newer version of this flow is available", null);
+                } else if (modified) {
+                    return new StandardVersionedFlowStatus(VersionedFlowState.LOCALLY_MODIFIED, "Local changes have been made", null);
+                } else if (stale) {
+                    return new StandardVersionedFlowStatus(VersionedFlowState.STALE, "A newer version of this flow is available", null);
+                } else {
+                    return new StandardVersionedFlowStatus(VersionedFlowState.UP_TO_DATE, "Flow version is current", null);
+                }
             }
         };
 
@@ -2875,6 +3028,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         try {
             updateVersionedComponentIds(this, versionedComponentIds);
             this.versionControlInfo.set(svci);
+            clearFlowDifferences();
         } finally {
             writeLock.unlock();
         }
@@ -2901,6 +3055,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         copy.setProcessors(processGroup.getProcessors());
         copy.setRemoteProcessGroups(processGroup.getRemoteProcessGroups());
         copy.setVariables(processGroup.getVariables());
+        copy.setLabels(processGroup.getLabels());
 
         final Set<VersionedProcessGroup> copyChildren = new HashSet<>();
 
@@ -2944,8 +3099,22 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
 
         applyVersionedComponentIds(processGroup, versionedComponentIds::get);
+
+        // If we versioned any parent groups' Controller Services, set their versioned component id's too.
+        final ProcessGroup parent = processGroup.getParent();
+        if (parent != null) {
+            for (final ControllerServiceNode service : parent.getControllerServices(true)) {
+                if (!service.getVersionedComponentId().isPresent()) {
+                    final String versionedId = versionedComponentIds.get(service.getIdentifier());
+                    if (versionedId != null) {
+                        service.setVersionedComponentId(versionedId);
+                    }
+                }
+            }
+        }
     }
 
+
     private void applyVersionedComponentIds(final ProcessGroup processGroup, final Function<String, String> lookup) {
         processGroup.setVersionedComponentId(lookup.apply(processGroup.getIdentifier()));
 
@@ -2980,6 +3149,14 @@ public final class StandardProcessGroup implements ProcessGroup {
             .forEach(childGroup -> applyVersionedComponentIds(childGroup, lookup));
     }
 
+    private void setSyncFailedState(final String explanation) {
+        boolean updated = false;
+        while (!updated) {
+            final StandardVersionedFlowStatus status = flowStatus.get();
+            final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(VersionedFlowState.SYNC_FAILURE, explanation, status.getCurrentDifferences());
+            updated = flowStatus.compareAndSet(status, updatedStatus);
+        }
+    }
 
     @Override
     public void synchronizeWithFlowRegistry(final FlowRegistryClient flowRegistryClient) {
@@ -2991,6 +3168,10 @@ public final class StandardProcessGroup implements ProcessGroup {
         final String registryId = vci.getRegistryIdentifier();
         final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
         if (flowRegistry == null) {
+            final String message = String.format("Unable to synchronize Process Group with Flow Registry because Process Group was placed under Version Control using Flow Registry "
+                + "with identifier %s but cannot find any Flow Registry with this identifier", registryId);
+            setSyncFailedState(message);
+
             LOG.error("Unable to synchronize {} with Flow Registry because Process Group was placed under Version Control using Flow Registry "
                 + "with identifier {} but cannot find any Flow Registry with this identifier", this, registryId);
             return;
@@ -3005,8 +3186,12 @@ public final class StandardProcessGroup implements ProcessGroup {
                 final VersionedProcessGroup registryFlow = registrySnapshot.getFlowContents();
                 vci.setFlowSnapshot(registryFlow);
             } catch (final IOException | NiFiRegistryException e) {
+                final String message = String.format("Failed to synchronize Process Group with Flow Registry because could not retrieve version %s of flow with identifier %s in bucket %s",
+                    vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier());
+                setSyncFailedState(message);
+
                 LOG.error("Failed to synchronize {} with Flow Registry because could not retrieve version {} of flow with identifier {} in bucket {}",
-                    new Object[] {this, vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier()}, e);
+                    this, vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier(), e);
                 return;
             }
         }
@@ -3027,7 +3212,17 @@ public final class StandardProcessGroup implements ProcessGroup {
                 LOG.info("{} is not the most recent version of the flow that is under Version Control; current version is {}; most recent version is {}",
                     new Object[] {this, vci.getVersion(), latestVersion});
             }
+
+            boolean updated = false;
+            while (!updated) {
+                final StandardVersionedFlowStatus status = flowStatus.get();
+                final StandardVersionedFlowStatus updatedStatus = new StandardVersionedFlowStatus(null, null, status.getCurrentDifferences());
+                updated = flowStatus.compareAndSet(status, updatedStatus);
+            }
         } catch (final IOException | NiFiRegistryException e) {
+            final String message = String.format("Failed to synchronize Process Group with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry");
+            setSyncFailedState(message);
+
             LOG.error("Failed to synchronize {} with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry", this, e);
         }
     }
@@ -3041,12 +3236,12 @@ public final class StandardProcessGroup implements ProcessGroup {
             verifyCanUpdate(proposedSnapshot, true, verifyNotDirty);
 
             final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-            final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), true);
+            final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, controllerServiceProvider, flowController.getFlowRegistryClient(), true);
 
             final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", versionedGroup);
             final ComparableDataFlow remoteFlow = new StandardComparableDataFlow("Remote Flow", proposedSnapshot.getFlowContents());
 
-            final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow, new StaticDifferenceDescriptor());
+            final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow, getAncestorGroupServiceIds(), new StaticDifferenceDescriptor());
             final FlowComparison flowComparison = flowComparator.compare();
 
             final Set<String> updatedVersionedComponentIds = new HashSet<>();
@@ -3055,6 +3250,25 @@ public final class StandardProcessGroup implements ProcessGroup {
                     continue;
                 }
 
+                // If this update adds a new Controller Service, then we need to check if the service already exists at a higher level
+                // and if so compare our VersionedControllerService to the existing service.
+                if (diff.getDifferenceType() == DifferenceType.COMPONENT_ADDED) {
+                    final VersionedComponent component = diff.getComponentA() == null ? diff.getComponentB() : diff.getComponentA();
+                    if (ComponentType.CONTROLLER_SERVICE == component.getComponentType()) {
+                        final ControllerServiceNode serviceNode = getVersionedControllerService(this, component.getIdentifier());
+                        if (serviceNode != null) {
+                            final VersionedControllerService versionedService = mapper.mapControllerService(serviceNode, controllerServiceProvider);
+                            final Set<FlowDifference> differences = flowComparator.compareControllerServices(versionedService, (VersionedControllerService) component);
+
+                            if (!differences.isEmpty()) {
+                                updatedVersionedComponentIds.add(component.getIdentifier());
+                            }
+
+                            continue;
+                        }
+                    }
+                }
+
                 final VersionedComponent component = diff.getComponentA() == null ? diff.getComponentB() : diff.getComponentA();
                 updatedVersionedComponentIds.add(component.getIdentifier());
 
@@ -3081,6 +3295,35 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    private Set<String> getAncestorGroupServiceIds() {
+        final Set<String> ancestorServiceIds;
+        ProcessGroup parentGroup = getParent();
+
+        if (parentGroup == null) {
+            ancestorServiceIds = Collections.emptySet();
+        } else {
+            ancestorServiceIds = parentGroup.getControllerServices(true).stream()
+                .map(ControllerServiceNode::getIdentifier)
+                .collect(Collectors.toSet());
+        }
+
+        return ancestorServiceIds;
+    }
+
+    private ControllerServiceNode getVersionedControllerService(final ProcessGroup group, final String versionedComponentId) {
+        if (group == null) {
+            return null;
+        }
+
+        for (final ControllerServiceNode serviceNode : group.getControllerServices(false)) {
+            if (serviceNode.getVersionedComponentId().isPresent() && serviceNode.getVersionedComponentId().get().equals(versionedComponentId)) {
+                return serviceNode;
+            }
+        }
+
+        return getVersionedControllerService(group.getParent(), versionedComponentId);
+    }
+
     private Set<String> getKnownVariableNames() {
         final Set<String> variableNames = new HashSet<>();
         populateKnownVariableNames(this, variableNames);
@@ -3159,6 +3402,44 @@ public final class StandardProcessGroup implements ProcessGroup {
             group.setVersionControlInformation(vci, Collections.emptyMap());
         }
 
+
+        // Controller Services
+        // Controller Services have to be handled a bit differently than other components. This is because Processors and Controller
+        // Services may reference other Controller Services. Since we may be adding Service A, which depends on Service B, before adding
+        // Service B, we need to ensure that we create all Controller Services first and then call updateControllerService for each
+        // Controller Service. This way, we ensure that all services have been created before setting the properties. This allows us to
+        // properly obtain the correct mapping of Controller Service VersionedComponentID to Controller Service instance id.
+        final Map<String, ControllerServiceNode> servicesByVersionedId = group.getControllerServices(false).stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+
+        final Set<String> controllerServicesRemoved = new HashSet<>(servicesByVersionedId.keySet());
+
+        final Map<ControllerServiceNode, VersionedControllerService> services = new HashMap<>();
+
+        // Add any Controller Service that does not yet exist.
+        for (final VersionedControllerService proposedService : proposed.getControllerServices()) {
+            ControllerServiceNode service = servicesByVersionedId.get(proposedService.getIdentifier());
+            if (service == null) {
+                service = addControllerService(group, proposedService, componentIdSeed);
+                LOG.info("Added {} to {}", service, this);
+            }
+
+            services.put(service, proposedService);
+        }
+
+        // Update all of the Controller Services to match the VersionedControllerService
+        for (final Map.Entry<ControllerServiceNode, VersionedControllerService> entry : services.entrySet()) {
+            final ControllerServiceNode service = entry.getKey();
+            final VersionedControllerService proposedService = entry.getValue();
+
+            if (updatedVersionedComponentIds.contains(proposedService.getIdentifier())) {
+                updateControllerService(service, proposedService);
+                LOG.info("Updated {}", service);
+            }
+
+            controllerServicesRemoved.remove(proposedService.getIdentifier());
+        }
+
         // Child groups
         final Map<String, ProcessGroup> childGroupsByVersionedId = group.getProcessGroups().stream()
             .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
@@ -3179,26 +3460,6 @@ public final class StandardProcessGroup implements ProcessGroup {
             childGroupsRemoved.remove(proposedChildGroup.getIdentifier());
         }
 
-
-        // Controller Services
-        final Map<String, ControllerServiceNode> servicesByVersionedId = group.getControllerServices(false).stream()
-            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
-        final Set<String> controllerServicesRemoved = new HashSet<>(servicesByVersionedId.keySet());
-
-        for (final VersionedControllerService proposedService : proposed.getControllerServices()) {
-            final ControllerServiceNode service = servicesByVersionedId.get(proposedService.getIdentifier());
-            if (service == null) {
-                final ControllerServiceNode added = addControllerService(group, proposedService, componentIdSeed);
-                LOG.info("Added {} to {}", added, this);
-            } else if (updatedVersionedComponentIds.contains(proposedService.getIdentifier())) {
-                updateControllerService(service, proposedService);
-                LOG.info("Updated {}", service);
-            }
-
-            controllerServicesRemoved.remove(proposedService.getIdentifier());
-        }
-
-
         // Funnels
         final Map<String, Funnel> funnelsByVersionedId = group.getFunnels().stream()
             .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
@@ -3608,7 +3869,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         service.setAnnotationData(proposed.getAnnotationData());
         service.setComments(proposed.getComments());
         service.setName(proposed.getName());
-        service.setProperties(populatePropertiesMap(service.getProperties(), proposed.getProperties()));
+        service.setProperties(populatePropertiesMap(service.getProperties(), proposed.getProperties(), proposed.getPropertyDescriptors(), service.getProcessGroup()));
 
         if (!isEqual(service.getBundleCoordinate(), proposed.getBundle())) {
             final BundleCoordinate newBundleCoordinate = toCoordinate(proposed.getBundle());
@@ -3728,7 +3989,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         processor.setExecutionNode(ExecutionNode.valueOf(proposed.getExecutionNode()));
         processor.setName(proposed.getName());
         processor.setPenalizationPeriod(proposed.getPenaltyDuration());
-        processor.setProperties(populatePropertiesMap(processor.getProperties(), proposed.getProperties()));
+        processor.setProperties(populatePropertiesMap(processor.getProperties(), proposed.getProperties(), proposed.getPropertyDescriptors(), processor.getProcessGroup()));
         processor.setRunDuration(proposed.getRunDurationMillis(), TimeUnit.MILLISECONDS);
         processor.setScheduldingPeriod(proposed.getSchedulingPeriod());
         processor.setSchedulingStrategy(SchedulingStrategy.valueOf(proposed.getSchedulingStrategy()));
@@ -3745,19 +4006,60 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
 
-    private Map<String, String> populatePropertiesMap(final Map<PropertyDescriptor, String> currentProperties, final Map<String, String> proposedProperties) {
+    private Map<String, String> populatePropertiesMap(final Map<PropertyDescriptor, String> currentProperties, final Map<String, String> proposedProperties,
+        final Map<String, VersionedPropertyDescriptor> proposedDescriptors, final ProcessGroup group) {
+
         final Map<String, String> fullPropertyMap = new HashMap<>();
         for (final PropertyDescriptor property : currentProperties.keySet()) {
             fullPropertyMap.put(property.getName(), null);
         }
 
         if (proposedProperties != null) {
-            fullPropertyMap.putAll(proposedProperties);
+            for (final Map.Entry<String, String> entry : proposedProperties.entrySet()) {
+                final String propertyName = entry.getKey();
+                final VersionedPropertyDescriptor descriptor = proposedDescriptors.get(propertyName);
+
+                String value;
+                if (descriptor != null && descriptor.getIdentifiesControllerService()) {
+                    // Property identifies a Controller Service. So the value that we want to assign is not the value given.
+                    // The value given is instead the Versioned Component ID of the Controller Service. We want to resolve this
+                    // to the instance ID of the Controller Service.
+                    final String serviceVersionedComponentId = entry.getValue();
+                    final String instanceId = getServiceInstanceId(serviceVersionedComponentId, group);
+                    value = instanceId == null ? serviceVersionedComponentId : instanceId;
+                } else {
+                    value = entry.getValue();
+                }
+
+                fullPropertyMap.put(propertyName, value);
+            }
         }
 
         return fullPropertyMap;
     }
 
+    private String getServiceInstanceId(final String serviceVersionedComponentId, final ProcessGroup group) {
+        for (final ControllerServiceNode serviceNode : group.getControllerServices(false)) {
+            final Optional<String> optionalVersionedId = serviceNode.getVersionedComponentId();
+            if (!optionalVersionedId.isPresent()) {
+                continue;
+            }
+
+            final String versionedId = optionalVersionedId.get();
+            if (versionedId.equals(serviceVersionedComponentId)) {
+                return serviceNode.getIdentifier();
+            }
+        }
+
+        final ProcessGroup parent = group.getParent();
+        if (parent == null) {
+            return null;
+        }
+
+        return getServiceInstanceId(serviceVersionedComponentId, parent);
+
+    }
+
     private RemoteProcessGroup addRemoteProcessGroup(final ProcessGroup destination, final VersionedRemoteProcessGroup proposed, final String componentIdSeed) {
         final RemoteProcessGroup rpg = flowController.createRemoteProcessGroup(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), proposed.getTargetUris());
         rpg.setVersionedComponentId(proposed.getIdentifier());
@@ -3773,12 +4075,12 @@ public final class StandardProcessGroup implements ProcessGroup {
         rpg.setCommunicationsTimeout(proposed.getCommunicationsTimeout());
         rpg.setInputPorts(proposed.getInputPorts() == null ? Collections.emptySet() : proposed.getInputPorts().stream()
             .map(port -> createPortDescriptor(port, componentIdSeed, rpg.getIdentifier()))
-            .collect(Collectors.toSet()));
+            .collect(Collectors.toSet()), false);
         rpg.setName(proposed.getName());
         rpg.setNetworkInterface(proposed.getLocalNetworkInterface());
         rpg.setOutputPorts(proposed.getOutputPorts() == null ? Collections.emptySet() : proposed.getOutputPorts().stream()
             .map(port -> createPortDescriptor(port, componentIdSeed, rpg.getIdentifier()))
-            .collect(Collectors.toSet()));
+            .collect(Collectors.toSet()), false);
         rpg.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
         rpg.setProxyHost(proposed.getProxyHost());
         rpg.setProxyPort(proposed.getProxyPort());
@@ -3831,12 +4133,12 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), false);
+        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, controllerServiceProvider, flowController.getFlowRegistryClient(), false);
 
         final ComparableDataFlow currentFlow = new StandardComparableDataFlow("Local Flow", versionedGroup);
         final ComparableDataFlow snapshotFlow = new StandardComparableDataFlow("Versioned Flow", vci.getFlowSnapshot());
 
-        final FlowComparator flowComparator = new StandardFlowComparator(snapshotFlow, currentFlow, new EvolvingDifferenceDescriptor());
+        final FlowComparator flowComparator = new StandardFlowComparator(snapshotFlow, currentFlow, getAncestorGroupServiceIds(), new EvolvingDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();
         final Set<FlowDifference> differences = comparison.getDifferences();
         final Set<FlowDifference> functionalDifferences = differences.stream()

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
new file mode 100644
index 0000000..f362c1e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardVersionedFlowStatus.java
@@ -0,0 +1,50 @@
+/*
+ * 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.groups;
+
+import java.util.Set;
+
+import org.apache.nifi.registry.flow.VersionedFlowState;
+import org.apache.nifi.registry.flow.VersionedFlowStatus;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+
+class StandardVersionedFlowStatus implements VersionedFlowStatus {
+    private final VersionedFlowState state;
+    private final String explanation;
+    private final Set<FlowDifference> currentDifferences;
+
+    StandardVersionedFlowStatus(final VersionedFlowState state, final String explanation, final Set<FlowDifference> differences) {
+        this.state = state;
+        this.explanation = explanation;
+        this.currentDifferences = differences;
+    }
+
+    @Override
+    public VersionedFlowState getState() {
+        return state;
+    }
+
+    @Override
+    public String getStateExplanation() {
+        return explanation;
+    }
+
+    Set<FlowDifference> getCurrentDifferences() {
+        return currentDifferences;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
index 92a4166..106d19a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
@@ -34,6 +34,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
     private volatile VersionedProcessGroup flowSnapshot;
     private volatile boolean modified;
     private volatile boolean current;
+    private final VersionedFlowStatus status;
 
     public static class Builder {
         private String registryIdentifier;
@@ -47,6 +48,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
         private VersionedProcessGroup flowSnapshot;
         private Boolean modified = null;
         private Boolean current = null;
+        private VersionedFlowStatus status;
 
         public Builder registryId(String registryId) {
             this.registryIdentifier = registryId;
@@ -103,6 +105,11 @@ public class StandardVersionControlInformation implements VersionControlInformat
             return this;
         }
 
+        public Builder status(final VersionedFlowStatus status) {
+            this.status = status;
+            return this;
+        }
+
         public static Builder fromDto(VersionControlInformationDTO dto) {
             Builder builder = new Builder();
             builder.registryId(dto.getRegistryId())
@@ -126,7 +133,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
             Objects.requireNonNull(version, "Version must be specified");
 
             final StandardVersionControlInformation svci = new StandardVersionControlInformation(registryIdentifier, registryName,
-                bucketIdentifier, flowIdentifier, version, flowSnapshot, modified, current);
+                bucketIdentifier, flowIdentifier, version, flowSnapshot, modified, current, status);
 
             svci.setBucketName(bucketName);
             svci.setFlowName(flowName);
@@ -138,7 +145,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
 
 
     public StandardVersionControlInformation(final String registryId, final String registryName, final String bucketId, final String flowId, final int version,
-        final VersionedProcessGroup snapshot, final boolean modified, final boolean current) {
+        final VersionedProcessGroup snapshot, final boolean modified, final boolean current, final VersionedFlowStatus status) {
         this.registryIdentifier = registryId;
         this.registryName = registryName;
         this.bucketIdentifier = bucketId;
@@ -147,6 +154,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
         this.flowSnapshot = snapshot;
         this.modified = modified;
         this.current = current;
+        this.status = status;
     }
 
 
@@ -232,4 +240,9 @@ public class StandardVersionControlInformation implements VersionControlInformat
     public void setFlowSnapshot(final VersionedProcessGroup flowSnapshot) {
         this.flowSnapshot = flowSnapshot;
     }
+
+    @Override
+    public VersionedFlowStatus getStatus() {
+        return status;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
deleted file mode 100644
index 193bde8..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * 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.registry.flow.mapping;
-
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-import org.apache.nifi.registry.flow.BatchSize;
-import org.apache.nifi.registry.flow.Bundle;
-import org.apache.nifi.registry.flow.ComponentType;
-import org.apache.nifi.registry.flow.ConnectableComponent;
-import org.apache.nifi.registry.flow.ConnectableComponentType;
-import org.apache.nifi.registry.flow.ControllerServiceAPI;
-import org.apache.nifi.registry.flow.PortType;
-import org.apache.nifi.registry.flow.Position;
-import org.apache.nifi.registry.flow.VersionedConnection;
-import org.apache.nifi.registry.flow.VersionedControllerService;
-import org.apache.nifi.registry.flow.VersionedFunnel;
-import org.apache.nifi.registry.flow.VersionedLabel;
-import org.apache.nifi.registry.flow.VersionedPort;
-import org.apache.nifi.registry.flow.VersionedProcessGroup;
-import org.apache.nifi.registry.flow.VersionedProcessor;
-import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
-import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
-import org.apache.nifi.web.api.dto.BatchSettingsDTO;
-import org.apache.nifi.web.api.dto.BundleDTO;
-import org.apache.nifi.web.api.dto.ConnectableDTO;
-import org.apache.nifi.web.api.dto.ConnectionDTO;
-import org.apache.nifi.web.api.dto.ControllerServiceApiDTO;
-import org.apache.nifi.web.api.dto.ControllerServiceDTO;
-import org.apache.nifi.web.api.dto.FlowSnippetDTO;
-import org.apache.nifi.web.api.dto.FunnelDTO;
-import org.apache.nifi.web.api.dto.LabelDTO;
-import org.apache.nifi.web.api.dto.PortDTO;
-import org.apache.nifi.web.api.dto.PositionDTO;
-import org.apache.nifi.web.api.dto.ProcessGroupDTO;
-import org.apache.nifi.web.api.dto.ProcessorConfigDTO;
-import org.apache.nifi.web.api.dto.ProcessorDTO;
-import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
-import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
-
-
-public class NiFiRegistryDtoMapper {
-    // We need to keep a mapping of component id to versionedComponentId as we transform these objects. This way, when
-    // we call #mapConnectable, instead of generating a new UUID for the ConnectableComponent, we can lookup the 'versioned'
-    // identifier based on the comopnent's actual id. We do connections last, so that all components will already have been
-    // created before attempting to create the connection, where the ConnectableDTO is converted.
-    private Map<String, String> versionedComponentIds = new HashMap<>();
-
-    public VersionedProcessGroup mapProcessGroup(final ProcessGroupDTO dto) {
-        versionedComponentIds.clear();
-        return mapGroup(dto);
-    }
-
-    private VersionedProcessGroup mapGroup(final ProcessGroupDTO dto) {
-        final VersionedProcessGroup versionedGroup = new VersionedProcessGroup();
-        versionedGroup.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        versionedGroup.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        versionedGroup.setName(dto.getName());
-        versionedGroup.setComments(dto.getComments());
-        versionedGroup.setPosition(mapPosition(dto.getPosition()));
-
-        final FlowSnippetDTO contents = dto.getContents();
-
-        versionedGroup.setControllerServices(contents.getControllerServices().stream()
-            .map(this::mapControllerService)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setFunnels(contents.getFunnels().stream()
-            .map(this::mapFunnel)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setInputPorts(contents.getInputPorts().stream()
-            .map(this::mapPort)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setOutputPorts(contents.getOutputPorts().stream()
-            .map(this::mapPort)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setLabels(contents.getLabels().stream()
-            .map(this::mapLabel)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setProcessors(contents.getProcessors().stream()
-            .map(this::mapProcessor)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setRemoteProcessGroups(contents.getRemoteProcessGroups().stream()
-            .map(this::mapRemoteProcessGroup)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setProcessGroups(contents.getProcessGroups().stream()
-            .map(this::mapGroup)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        versionedGroup.setConnections(contents.getConnections().stream()
-            .map(this::mapConnection)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-
-        return versionedGroup;
-    }
-
-    private String getId(final String currentVersionedId, final String componentId) {
-        final String versionedId;
-        if (currentVersionedId == null) {
-            versionedId = UUID.nameUUIDFromBytes(componentId.getBytes(StandardCharsets.UTF_8)).toString();
-        } else {
-            versionedId = currentVersionedId;
-        }
-
-        versionedComponentIds.put(componentId, versionedId);
-        return versionedId;
-    }
-
-    private String getGroupId(final String groupId) {
-        return versionedComponentIds.get(groupId);
-    }
-
-    public VersionedConnection mapConnection(final ConnectionDTO dto) {
-        final VersionedConnection connection = new VersionedConnection();
-        connection.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        connection.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        connection.setName(dto.getName());
-        connection.setBackPressureDataSizeThreshold(dto.getBackPressureDataSizeThreshold());
-        connection.setBackPressureObjectThreshold(dto.getBackPressureObjectThreshold());
-        connection.setFlowFileExpiration(dto.getFlowFileExpiration());
-        connection.setLabelIndex(dto.getLabelIndex());
-        connection.setPosition(mapPosition(dto.getPosition()));
-        connection.setPrioritizers(dto.getPrioritizers());
-        connection.setSelectedRelationships(dto.getSelectedRelationships());
-        connection.setzIndex(dto.getzIndex());
-
-        connection.setBends(dto.getBends().stream()
-            .map(this::mapPosition)
-            .collect(Collectors.toList()));
-
-        connection.setSource(mapConnectable(dto.getSource()));
-        connection.setDestination(mapConnectable(dto.getDestination()));
-
-        return connection;
-    }
-
-    public ConnectableComponent mapConnectable(final ConnectableDTO dto) {
-        final ConnectableComponent component = new ConnectableComponent();
-
-        final String versionedId = dto.getVersionedComponentId();
-        if (versionedId == null) {
-            final String resolved = versionedComponentIds.get(dto.getId());
-            if (resolved == null) {
-                throw new IllegalArgumentException("Unable to map Connectable Component with identifier " + dto.getId() + " to any version-controlled component");
-            }
-
-            component.setId(resolved);
-        } else {
-            component.setId(versionedId);
-        }
-
-        component.setComments(dto.getComments());
-        component.setGroupId(dto.getGroupId());
-        component.setName(dto.getName());
-        component.setType(ConnectableComponentType.valueOf(dto.getType()));
-        return component;
-    }
-
-    public VersionedControllerService mapControllerService(final ControllerServiceDTO dto) {
-        final VersionedControllerService service = new VersionedControllerService();
-        service.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        service.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        service.setName(dto.getName());
-        service.setAnnotationData(dto.getAnnotationData());
-        service.setBundle(mapBundle(dto.getBundle()));
-        service.setComments(dto.getComments());
-        service.setControllerServiceApis(dto.getControllerServiceApis().stream()
-            .map(this::mapControllerServiceApi)
-            .collect(Collectors.toList()));
-        service.setProperties(dto.getProperties());
-        service.setType(dto.getType());
-        return null;
-    }
-
-    private Bundle mapBundle(final BundleDTO dto) {
-        final Bundle bundle = new Bundle();
-        bundle.setGroup(dto.getGroup());
-        bundle.setArtifact(dto.getArtifact());
-        bundle.setVersion(dto.getVersion());
-        return bundle;
-    }
-
-    private ControllerServiceAPI mapControllerServiceApi(final ControllerServiceApiDTO dto) {
-        final ControllerServiceAPI api = new ControllerServiceAPI();
-        api.setBundle(mapBundle(dto.getBundle()));
-        api.setType(dto.getType());
-        return api;
-    }
-
-    public VersionedFunnel mapFunnel(final FunnelDTO dto) {
-        final VersionedFunnel funnel = new VersionedFunnel();
-        funnel.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        funnel.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        funnel.setPosition(mapPosition(dto.getPosition()));
-        return funnel;
-    }
-
-    public VersionedLabel mapLabel(final LabelDTO dto) {
-        final VersionedLabel label = new VersionedLabel();
-        label.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        label.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        label.setHeight(dto.getHeight());
-        label.setWidth(dto.getWidth());
-        label.setLabel(dto.getLabel());
-        label.setPosition(mapPosition(dto.getPosition()));
-        label.setStyle(dto.getStyle());
-        return label;
-    }
-
-    public VersionedPort mapPort(final PortDTO dto) {
-        final VersionedPort port = new VersionedPort();
-        port.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        port.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        port.setComments(dto.getComments());
-        port.setConcurrentlySchedulableTaskCount(dto.getConcurrentlySchedulableTaskCount());
-        port.setName(dto.getName());
-        port.setPosition(mapPosition(dto.getPosition()));
-        port.setType(PortType.valueOf(dto.getType()));
-        return port;
-    }
-
-    public Position mapPosition(final PositionDTO dto) {
-        final Position position = new Position();
-        position.setX(dto.getX());
-        position.setY(dto.getY());
-        return position;
-    }
-
-    public VersionedProcessor mapProcessor(final ProcessorDTO dto) {
-        final ProcessorConfigDTO config = dto.getConfig();
-
-        final VersionedProcessor processor = new VersionedProcessor();
-        processor.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        processor.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        processor.setType(dto.getType());
-        processor.setAnnotationData(config.getAnnotationData());
-        processor.setAutoTerminatedRelationships(config.getAutoTerminatedRelationships());
-        processor.setBulletinLevel(config.getBulletinLevel());
-        processor.setBundle(mapBundle(dto.getBundle()));
-        processor.setComments(config.getComments());
-        processor.setConcurrentlySchedulableTaskCount(config.getConcurrentlySchedulableTaskCount());
-        processor.setExecutionNode(config.getExecutionNode());
-        processor.setName(dto.getName());
-        processor.setPenaltyDuration(config.getPenaltyDuration());
-        processor.setPosition(mapPosition(dto.getPosition()));
-        processor.setProperties(config.getProperties());
-        processor.setRunDurationMillis(config.getRunDurationMillis());
-        processor.setSchedulingPeriod(config.getSchedulingPeriod());
-        processor.setSchedulingStrategy(config.getSchedulingStrategy());
-        processor.setStyle(dto.getStyle());
-        processor.setYieldDuration(config.getYieldDuration());
-        return processor;
-    }
-
-    public VersionedRemoteProcessGroup mapRemoteProcessGroup(final RemoteProcessGroupDTO dto) {
-        final VersionedRemoteProcessGroup rpg = new VersionedRemoteProcessGroup();
-        rpg.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        rpg.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
-        rpg.setComments(dto.getComments());
-        rpg.setCommunicationsTimeout(dto.getCommunicationsTimeout());
-        rpg.setLocalNetworkInterface(dto.getLocalNetworkInterface());
-        rpg.setName(dto.getName());
-        rpg.setInputPorts(dto.getContents().getInputPorts().stream()
-            .map(port -> mapRemotePort(port, ComponentType.REMOTE_INPUT_PORT))
-            .collect(Collectors.toSet()));
-        rpg.setOutputPorts(dto.getContents().getOutputPorts().stream()
-            .map(port -> mapRemotePort(port, ComponentType.REMOTE_OUTPUT_PORT))
-            .collect(Collectors.toSet()));
-        rpg.setPosition(mapPosition(dto.getPosition()));
-        rpg.setProxyHost(dto.getProxyHost());
-        rpg.setProxyPort(dto.getProxyPort());
-        rpg.setProxyUser(dto.getProxyUser());
-        rpg.setTargetUri(dto.getTargetUri());
-        rpg.setTargetUris(dto.getTargetUris());
-        rpg.setTransportProtocol(dto.getTransportProtocol());
-        rpg.setYieldDuration(dto.getYieldDuration());
-        return rpg;
-    }
-
-    public VersionedRemoteGroupPort mapRemotePort(final RemoteProcessGroupPortDTO dto, final ComponentType componentType) {
-        final VersionedRemoteGroupPort port = new VersionedRemoteGroupPort();
-        port.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
-        port.setGroupIdentifier(getGroupId(dto.getGroupId()));
-        port.setComments(dto.getComments());
-        port.setConcurrentlySchedulableTaskCount(dto.getConcurrentlySchedulableTaskCount());
-        port.setRemoteGroupId(dto.getGroupId());
-        port.setName(dto.getName());
-        port.setUseCompression(dto.getUseCompression());
-        port.setBatchSize(mapBatchSettings(dto.getBatchSettings()));
-        port.setTargetId(dto.getTargetId());
-        port.setComponentType(componentType);
-        return port;
-    }
-
-    private BatchSize mapBatchSettings(final BatchSettingsDTO dto) {
-        final BatchSize batchSize = new BatchSize();
-        batchSize.setCount(dto.getCount());
-        batchSize.setDuration(dto.getDuration());
-        batchSize.setSize(dto.getSize());
-        return batchSize;
-    }
-}


[14/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index ec32cc1..282c50d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -16,7 +16,30 @@
  */
 package org.apache.nifi.groups;
 
-import com.google.common.collect.Sets;
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -29,6 +52,7 @@ import org.apache.nifi.authorization.Resource;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.resource.ResourceFactory;
 import org.apache.nifi.authorization.resource.ResourceType;
+import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.state.StateManager;
 import org.apache.nifi.components.state.StateManagerProvider;
@@ -40,6 +64,7 @@ import org.apache.nifi.connectable.LocalPort;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.connectable.Position;
 import org.apache.nifi.connectable.Positionable;
+import org.apache.nifi.connectable.Size;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.ConfiguredComponent;
 import org.apache.nifi.controller.ControllerService;
@@ -49,22 +74,58 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.Snippet;
 import org.apache.nifi.controller.Template;
 import org.apache.nifi.controller.exception.ComponentLifeCycleException;
+import org.apache.nifi.controller.exception.ProcessorInstantiationException;
 import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.queue.FlowFileQueue;
 import org.apache.nifi.controller.scheduling.StandardProcessScheduler;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.controller.service.ControllerServiceReference;
 import org.apache.nifi.controller.service.StandardConfigurationContext;
 import org.apache.nifi.encrypt.StringEncryptor;
+import org.apache.nifi.flowfile.FlowFilePrioritizer;
+import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.logging.LogRepositoryFactory;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.StandardProcessContext;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableDescriptor;
+import org.apache.nifi.registry.flow.Bundle;
+import org.apache.nifi.registry.flow.ConnectableComponent;
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.RemoteFlowCoordinates;
+import org.apache.nifi.registry.flow.StandardVersionControlInformation;
+import org.apache.nifi.registry.flow.UnknownResourceException;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedConnection;
+import org.apache.nifi.registry.flow.VersionedControllerService;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFunnel;
+import org.apache.nifi.registry.flow.VersionedLabel;
+import org.apache.nifi.registry.flow.VersionedPort;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
+import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
+import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.DifferenceType;
+import org.apache.nifi.registry.flow.diff.FlowComparator;
+import org.apache.nifi.registry.flow.diff.FlowComparison;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
+import org.apache.nifi.remote.StandardRemoteProcessGroupPortDescriptor;
+import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.util.ComponentIdGenerator;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.ReflectionUtils;
 import org.apache.nifi.web.Revision;
@@ -72,23 +133,6 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
-
-import static java.util.Objects.requireNonNull;
-
 public final class StandardProcessGroup implements ProcessGroup {
 
     private final String id;
@@ -96,6 +140,8 @@ public final class StandardProcessGroup implements ProcessGroup {
     private final AtomicReference<String> name;
     private final AtomicReference<Position> position;
     private final AtomicReference<String> comments;
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
+    private final AtomicReference<StandardVersionControlInformation> versionControlInfo = new AtomicReference<>();
 
     private final StandardProcessScheduler scheduler;
     private final ControllerServiceProvider controllerServiceProvider;
@@ -782,7 +828,6 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             removed = true;
             LOG.info("{} removed from flow", processor);
-
         } finally {
             if (removed) {
                 try {
@@ -1935,7 +1980,6 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             removed = true;
             LOG.info("{} removed from {}", service, this);
-
         } finally {
             if (removed) {
                 try {
@@ -2234,7 +2278,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
     @Override
     public Set<Positionable> findAllPositionables() {
-        Set<Positionable> positionables = Sets.newHashSet();
+        final Set<Positionable> positionables = new HashSet<>();
         positionables.addAll(findAllConnectables(this, true));
         List<ProcessGroup> allProcessGroups = findAllProcessGroups();
         positionables.addAll(allProcessGroups);
@@ -2371,7 +2415,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 connection.verifyCanDelete();
             }
 
-            for(final ControllerServiceNode cs : controllerServices.values()) {
+            for (final ControllerServiceNode cs : controllerServices.values()) {
                 cs.verifyCanDelete();
             }
 
@@ -2647,6 +2691,31 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String componentId) {
+        writeLock.lock();
+        try {
+            final String currentId = versionedComponentId.get();
+
+            if (currentId == null) {
+                versionedComponentId.set(componentId);
+            } else if (currentId.equals(componentId)) {
+                return;
+            } else if (componentId == null) {
+                versionedComponentId.set(null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control with a different Versioned Component ID");
+            }
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
     public Set<ConfiguredComponent> getComponentsAffectedByVariable(final String variableName) {
         final Set<ConfiguredComponent> affected = new HashSet<>();
 
@@ -2736,4 +2805,1072 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    @Override
+    public VersionControlInformation getVersionControlInformation() {
+        return versionControlInfo.get();
+    }
+
+    @Override
+    public void setVersionControlInformation(final VersionControlInformation versionControlInformation, final Map<String, String> versionedComponentIds) {
+        final StandardVersionControlInformation svci = new StandardVersionControlInformation(versionControlInformation.getRegistryIdentifier(),
+            versionControlInformation.getBucketIdentifier(),
+            versionControlInformation.getFlowIdentifier(),
+            versionControlInformation.getVersion(),
+            versionControlInformation.getFlowSnapshot(),
+            versionControlInformation.getModified().orElse(null),
+            versionControlInformation.getCurrent().orElse(null)) {
+
+            @Override
+            public Optional<Boolean> getModified() {
+                return StandardProcessGroup.this.isModified();
+            }
+        };
+
+        writeLock.lock();
+        try {
+            updateVersionedComponentIds(this, versionedComponentIds);
+            this.versionControlInfo.set(svci);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    private void updateVersionedComponentIds(final ProcessGroup processGroup, final Map<String, String> versionedComponentIds) {
+        if (versionedComponentIds == null || versionedComponentIds.isEmpty()) {
+            return;
+        }
+
+        processGroup.setVersionedComponentId(versionedComponentIds.get(processGroup.getIdentifier()));
+
+        processGroup.getConnections().stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+        processGroup.getProcessors().stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+        processGroup.getInputPorts().stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+        processGroup.getOutputPorts().stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+        processGroup.getLabels().stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+        processGroup.getFunnels().stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+        processGroup.getControllerServices(false).stream()
+            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+
+        processGroup.getRemoteProcessGroups().stream()
+            .forEach(rpg -> {
+                rpg.setVersionedComponentId(versionedComponentIds.get(rpg.getIdentifier()));
+
+                rpg.getInputPorts().stream()
+                    .forEach(port -> port.setVersionedComponentId(versionedComponentIds.get(port.getIdentifier())));
+
+                rpg.getOutputPorts().stream()
+                    .forEach(port -> port.setVersionedComponentId(versionedComponentIds.get(port.getIdentifier())));
+            });
+
+        processGroup.getProcessGroups().stream()
+            .forEach(childGroup -> updateVersionedComponentIds(childGroup, versionedComponentIds));
+    }
+
+
+    @Override
+    public void synchronizeWithFlowRegistry(final FlowRegistryClient flowRegistryClient) {
+        final StandardVersionControlInformation vci = versionControlInfo.get();
+        if (vci == null) {
+            return;
+        }
+
+        final String registryId = vci.getRegistryIdentifier();
+        final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
+        if (flowRegistry == null) {
+            LOG.error("Unable to synchronize {} with Flow Registry because Process Group was placed under Version Control using Flow Registry "
+                + "with identifier {} but cannot find any Flow Registry with this identifier", this, registryId);
+            return;
+        }
+
+        try {
+            final int latestVersion = flowRegistry.getLatestVersion(vci.getBucketIdentifier(), vci.getFlowIdentifier());
+
+            if (latestVersion == vci.getVersion()) {
+                LOG.debug("{} is currently at the most recent version ({}) of the flow that is under Version Control", this, latestVersion);
+                vci.setCurrent(true);
+            } else {
+                vci.setCurrent(false);
+                LOG.info("{} is not the most recent version of the flow that is under Version Control; current version is {}; most recent version is {}",
+                    new Object[] {this, vci.getVersion(), latestVersion});
+            }
+        } catch (final IOException | UnknownResourceException e) {
+            LOG.error("Failed to synchronize {} with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry", this, e);
+        }
+
+        final VersionedProcessGroup snapshot = vci.getFlowSnapshot();
+        if (snapshot == null) {
+            // We have not yet obtained the snapshot from the Flow Registry, so we need to request the snapshot of our local version of the flow from the Flow Registry.
+            // This allows us to know whether or not the flow has been modified since it was last synced with the Flow Registry.
+            try {
+                final VersionedFlowSnapshot registrySnapshot = flowRegistry.getFlowContents(vci.getBucketIdentifier(), vci.getFlowIdentifier(), vci.getVersion());
+                final VersionedProcessGroup registryFlow = registrySnapshot.getFlowContents();
+                vci.setFlowSnapshot(registryFlow);
+            } catch (final IOException | UnknownResourceException e) {
+                LOG.error("Failed to synchronize {} with Flow Registry because could not retrieve version {} of flow with identifier {} in bucket {}",
+                    new Object[] {this, vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier()}, e);
+                return;
+            }
+        }
+    }
+
+
+    @Override
+    public void updateFlow(final VersionedFlowSnapshot proposedSnapshot, final String componentIdSeed, final boolean verifyNotDirty) {
+        writeLock.lock();
+        try {
+            verifyCanUpdate(proposedSnapshot, true, verifyNotDirty); // TODO: Should perform more verification... verifyCanDelete, verifyCanUpdate, etc. Recursively if child is under VC also
+
+            final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
+            final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient());
+
+            final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", versionedGroup);
+            final ComparableDataFlow remoteFlow = new StandardComparableDataFlow("Remote Flow", proposedSnapshot.getFlowContents());
+
+            final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow);
+            final FlowComparison flowComparison = flowComparator.compare();
+
+            final Set<String> updatedVersionedComponentIds = flowComparison.getDifferences().stream()
+                .filter(diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED)
+                .map(diff -> diff.getComponentA() == null ? diff.getComponentB().getIdentifier() : diff.getComponentA().getIdentifier())
+                .collect(Collectors.toSet());
+
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Updating {} to {}; there are {} differences to take into account: {}", this, proposedSnapshot, flowComparison.getDifferences().size(), flowComparison.getDifferences());
+            } else {
+                // TODO: Remove the actual differences from the info level log. It can be extremely verbose. Is here only for testing purposes becuase it's much more convenient
+                // than having to remember to enable DEBUG level logging every time a full build is done.
+                LOG.info("Updating {} to {}; there are {} differences to take into account: {}", this, proposedSnapshot, flowComparison.getDifferences().size(), flowComparison.getDifferences());
+            }
+
+            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false);
+        } catch (final ProcessorInstantiationException pie) {
+            throw new RuntimeException(pie);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+
+    private void updateProcessGroup(final ProcessGroup group, final VersionedProcessGroup proposed, final String componentIdSeed,
+        final Set<String> updatedVersionedComponentIds, final boolean updatePosition) throws ProcessorInstantiationException {
+
+        group.setComments(proposed.getComments());
+        group.setName(proposed.getName());
+        if (updatePosition && proposed.getPosition() != null) {
+            group.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
+        }
+
+        // Determine which variables have been added/removed and add/remove them from this group's variable registry.
+        // We don't worry about if a variable value has changed, because variables are designed to be 'environment specific.'
+        // As a result, once imported, we won't update variables to match the remote flow, but we will add any missing variables
+        // and remove any variables that are no longer part of the remote flow.
+        final Set<String> existingVariableNames = group.getVariableRegistry().getVariableMap().keySet().stream()
+            .map(VariableDescriptor::getName)
+            .collect(Collectors.toSet());
+
+        final Set<String> variablesRemoved = new HashSet<>(existingVariableNames);
+        variablesRemoved.removeAll(proposed.getVariables().keySet());
+        final Map<String, String> updatedVariableMap = new HashMap<>();
+        variablesRemoved.forEach(var -> updatedVariableMap.put(var, null));
+
+        // If any new variables exist in the proposed flow, add those to the variable registry.
+        for (final Map.Entry<String, String> entry : proposed.getVariables().entrySet()) {
+            if (!existingVariableNames.contains(entry.getKey())) {
+                updatedVariableMap.put(entry.getKey(), entry.getValue());
+            }
+        }
+
+        group.setVariables(updatedVariableMap);
+
+        final RemoteFlowCoordinates remoteCoordinates = proposed.getRemoteFlowCoordinates();
+        if (remoteCoordinates != null) {
+            final String registryId = flowController.getFlowRegistryClient().getFlowRegistryId(remoteCoordinates.getRegistryUrl());
+            final String bucketId = remoteCoordinates.getBucketId();
+            final String flowId = remoteCoordinates.getFlowId();
+            final int version = remoteCoordinates.getVersion();
+
+            final VersionControlInformation vci = new StandardVersionControlInformation(registryId, bucketId, flowId, version, proposed, false, true);
+            group.setVersionControlInformation(vci, Collections.emptyMap());
+        }
+
+        // Child groups
+        // TODO: Need to take into account if child group is under version control pointing to a different Versioned Flow and if so need to handle it differently.
+        final Map<String, ProcessGroup> childGroupsByVersionedId = group.getProcessGroups().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> childGroupsRemoved = new HashSet<>(childGroupsByVersionedId.keySet());
+
+        for (final VersionedProcessGroup proposedChildGroup : proposed.getProcessGroups()) {
+            final ProcessGroup childGroup = childGroupsByVersionedId.get(proposedChildGroup.getIdentifier());
+
+            if (childGroup == null) {
+                final ProcessGroup added = addProcessGroup(proposedChildGroup, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else {
+                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true);
+                LOG.info("Updated {}", childGroup);
+            }
+
+            childGroupsRemoved.remove(proposedChildGroup.getIdentifier());
+        }
+
+
+        // Controller Services
+        final Map<String, ControllerServiceNode> servicesByVersionedId = group.getControllerServices(false).stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> controllerServicesRemoved = new HashSet<>(servicesByVersionedId.keySet());
+
+        for (final VersionedControllerService proposedService : proposed.getControllerServices()) {
+            final ControllerServiceNode service = servicesByVersionedId.get(proposedService.getIdentifier());
+            if (service == null) {
+                final ControllerServiceNode added = addControllerService(proposedService, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedService.getIdentifier())) {
+                updateControllerService(service, proposedService);
+                LOG.info("Updated {}", service);
+            }
+
+            controllerServicesRemoved.remove(proposedService.getIdentifier());
+        }
+
+
+        // Funnels
+        final Map<String, Funnel> funnelsByVersionedId = group.getFunnels().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> funnelsRemoved = new HashSet<>(funnelsByVersionedId.keySet());
+
+        for (final VersionedFunnel proposedFunnel : proposed.getFunnels()) {
+            final Funnel funnel = funnelsByVersionedId.get(proposedFunnel.getIdentifier());
+            if (funnel == null) {
+                final Funnel added = addFunnel(proposedFunnel, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedFunnel.getIdentifier())) {
+                updateFunnel(funnel, proposedFunnel);
+                LOG.info("Updated {}", funnel);
+            } else {
+                funnel.setPosition(new Position(proposedFunnel.getPosition().getX(), proposedFunnel.getPosition().getY()));
+            }
+
+            funnelsRemoved.remove(proposedFunnel.getIdentifier());
+        }
+
+
+        // Input Ports
+        final Map<String, Port> inputPortsByVersionedId = group.getInputPorts().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> inputPortsRemoved = new HashSet<>(inputPortsByVersionedId.keySet());
+
+        for (final VersionedPort proposedPort : proposed.getInputPorts()) {
+            final Port port = inputPortsByVersionedId.get(proposedPort.getIdentifier());
+            if (port == null) {
+                final Port added = addInputPort(proposedPort, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedPort.getIdentifier())) {
+                updatePort(port, proposedPort);
+                LOG.info("Updated {}", port);
+            } else {
+                port.setPosition(new Position(proposedPort.getPosition().getX(), proposedPort.getPosition().getY()));
+            }
+
+            inputPortsRemoved.remove(proposedPort.getIdentifier());
+        }
+
+        // Output Ports
+        final Map<String, Port> outputPortsByVersionedId = group.getOutputPorts().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> outputPortsRemoved = new HashSet<>(outputPortsByVersionedId.keySet());
+
+        for (final VersionedPort proposedPort : proposed.getOutputPorts()) {
+            final Port port = outputPortsByVersionedId.get(proposedPort.getIdentifier());
+            if (port == null) {
+                final Port added = addOutputPort(proposedPort, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedPort.getIdentifier())) {
+                updatePort(port, proposedPort);
+                LOG.info("Updated {}", port);
+            } else {
+                port.setPosition(new Position(proposedPort.getPosition().getX(), proposedPort.getPosition().getY()));
+            }
+
+            outputPortsRemoved.remove(proposedPort.getIdentifier());
+        }
+
+
+        // Labels
+        final Map<String, Label> labelsByVersionedId = group.getLabels().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> labelsRemoved = new HashSet<>(labelsByVersionedId.keySet());
+
+        for (final VersionedLabel proposedLabel : proposed.getLabels()) {
+            final Label label = labelsByVersionedId.get(proposedLabel.getIdentifier());
+            if (label == null) {
+                final Label added = addLabel(proposedLabel, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedLabel.getIdentifier())) {
+                updateLabel(label, proposedLabel);
+                LOG.info("Updated {}", label);
+            } else {
+                label.setPosition(new Position(proposedLabel.getPosition().getX(), proposedLabel.getPosition().getY()));
+            }
+
+            labelsRemoved.remove(proposedLabel.getIdentifier());
+        }
+
+
+        // Processors
+        final Map<String, ProcessorNode> processorsByVersionedId = group.getProcessors().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> processorsRemoved = new HashSet<>(processorsByVersionedId.keySet());
+
+        for (final VersionedProcessor proposedProcessor : proposed.getProcessors()) {
+            final ProcessorNode processor = processorsByVersionedId.get(proposedProcessor.getIdentifier());
+            if (processor == null) {
+                final ProcessorNode added = addProcessor(proposedProcessor, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedProcessor.getIdentifier())) {
+                updateProcessor(processor, proposedProcessor);
+                LOG.info("Updated {}", processor);
+            } else {
+                processor.setPosition(new Position(proposedProcessor.getPosition().getX(), proposedProcessor.getPosition().getY()));
+            }
+
+            processorsRemoved.remove(proposedProcessor.getIdentifier());
+        }
+
+
+        // Remote Groups
+        final Map<String, RemoteProcessGroup> rpgsByVersionedId = group.getRemoteProcessGroups().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> rpgsRemoved = new HashSet<>(rpgsByVersionedId.keySet());
+
+        for (final VersionedRemoteProcessGroup proposedRpg : proposed.getRemoteProcessGroups()) {
+            final RemoteProcessGroup rpg = rpgsByVersionedId.get(proposedRpg.getIdentifier());
+            if (rpg == null) {
+                final RemoteProcessGroup added = addRemoteProcessGroup(proposedRpg, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (updatedVersionedComponentIds.contains(proposedRpg.getIdentifier())) {
+                updateRemoteProcessGroup(rpg, proposedRpg);
+                LOG.info("Updated {}", rpg);
+            } else {
+                rpg.setPosition(new Position(proposedRpg.getPosition().getX(), proposedRpg.getPosition().getY()));
+            }
+
+            rpgsRemoved.remove(proposedRpg.getIdentifier());
+        }
+
+
+        // Connections
+        final Map<String, Connection> connectionsByVersionedId = group.getConnections().stream()
+            .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
+        final Set<String> connectionsRemoved = new HashSet<>(connectionsByVersionedId.keySet());
+
+        for (final VersionedConnection proposedConnection : proposed.getConnections()) {
+            final Connection connection = connectionsByVersionedId.get(proposedConnection.getIdentifier());
+            if (connection == null) {
+                final Connection added = addConnection(proposedConnection, componentIdSeed);
+                LOG.info("Added {} to {}", added, this);
+            } else if (!connection.getSource().isRunning() && !connection.getDestination().isRunning()) {
+                // If the connection needs to be updated, then the source and destination will already have
+                // been stopped (else, the validation above would fail). So if the source or the destination is running,
+                // then we know that we don't need to update the connection.
+                updateConnection(connection, proposedConnection);
+                LOG.info("Updated {}", connection);
+            }
+
+            connectionsRemoved.remove(proposedConnection.getIdentifier());
+        }
+
+        // Remove components that exist in the local flow but not the remote flow.
+
+        // Connections must be the first thing to remove, not the last. Otherwise, we will fail
+        // to remove a component if it has a connection going to it!
+        for (final String removedVersionedId : connectionsRemoved) {
+            final Connection connection = connectionsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", connection, group);
+            group.removeConnection(connection);
+        }
+
+        for (final String removedVersionedId : controllerServicesRemoved) {
+            final ControllerServiceNode service = servicesByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", service, group);
+            group.removeControllerService(service);
+        }
+
+        for (final String removedVersionedId : funnelsRemoved) {
+            final Funnel funnel = funnelsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", funnel, group);
+            group.removeFunnel(funnel);
+        }
+
+        for (final String removedVersionedId : inputPortsRemoved) {
+            final Port port = inputPortsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", port, group);
+            group.removeInputPort(port);
+        }
+
+        for (final String removedVersionedId : outputPortsRemoved) {
+            final Port port = outputPortsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", port, group);
+            group.removeOutputPort(port);
+        }
+
+        for (final String removedVersionedId : labelsRemoved) {
+            final Label label = labelsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", label, group);
+            group.removeLabel(label);
+        }
+
+        for (final String removedVersionedId : processorsRemoved) {
+            final ProcessorNode processor = processorsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", processor, group);
+            group.removeProcessor(processor);
+        }
+
+        for (final String removedVersionedId : rpgsRemoved) {
+            final RemoteProcessGroup rpg = rpgsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", rpg, group);
+            group.removeRemoteProcessGroup(rpg);
+        }
+
+        for (final String removedVersionedId : childGroupsRemoved) {
+            final ProcessGroup childGroup = childGroupsByVersionedId.get(removedVersionedId);
+            LOG.info("Removing {} from {}", childGroup, group);
+            group.removeProcessGroup(childGroup);
+        }
+    }
+
+    protected String generateUuid(final String componentIdSeed) {
+        UUID uuid;
+        if (componentIdSeed == null) {
+            uuid = ComponentIdGenerator.generateId();
+        } else {
+            try {
+                UUID seedId = UUID.fromString(componentIdSeed);
+                uuid = new UUID(seedId.getMostSignificantBits(), componentIdSeed.hashCode());
+            } catch (Exception e) {
+                LOG.warn("Provided 'seed' does not represent UUID. Will not be able to extract most significant bits for ID generation.");
+                uuid = UUID.nameUUIDFromBytes(componentIdSeed.getBytes(StandardCharsets.UTF_8));
+            }
+        }
+
+        return uuid.toString();
+    }
+
+
+    private ProcessGroup addProcessGroup(final VersionedProcessGroup proposed, final String componentIdSeed) throws ProcessorInstantiationException {
+        final ProcessGroup group = flowController.createProcessGroup(generateUuid(componentIdSeed));
+        group.setVersionedComponentId(proposed.getIdentifier());
+        addProcessGroup(group);
+        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true);
+        return group;
+    }
+
+    private void updateConnection(final Connection connection, final VersionedConnection proposed) {
+        connection.setBendPoints(proposed.getBends().stream()
+            .map(pos -> new Position(pos.getX(), pos.getY()))
+            .collect(Collectors.toList()));
+
+        connection.setDestination(getConnectable(proposed.getDestination()));
+        connection.setLabelIndex(proposed.getLabelIndex());
+        connection.setName(proposed.getName());
+        connection.setRelationships(proposed.getSelectedRelationships().stream()
+            .map(name -> new Relationship.Builder().name(name).build())
+            .collect(Collectors.toSet()));
+        connection.setZIndex(proposed.getzIndex());
+
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+        queue.setBackPressureDataSizeThreshold(proposed.getBackPressureDataSizeThreshold());
+        queue.setBackPressureObjectThreshold(proposed.getBackPressureObjectThreshold());
+        queue.setFlowFileExpiration(proposed.getFlowFileExpiration());
+
+        final List<FlowFilePrioritizer> prioritizers = proposed.getPrioritizers().stream()
+            .map(prioritizerName -> {
+                try {
+                    return flowController.createPrioritizer(prioritizerName);
+                } catch (final Exception e) {
+                    throw new RuntimeException("Failed to create Prioritizer of type " + prioritizerName + " for Connection with ID " + connection.getIdentifier());
+                }
+            })
+            .collect(Collectors.toList());
+
+        queue.setPriorities(prioritizers);
+    }
+
+    private Connection addConnection(final VersionedConnection proposed, final String componentIdSeed) {
+        final Connectable source = getConnectable(proposed.getSource());
+        if (source == null) {
+            throw new IllegalArgumentException("Connection has a source with identifier " + proposed.getIdentifier()
+                + " but no component could be found in the Process Group with a corresponding identifier");
+        }
+
+        final Connectable destination = getConnectable(proposed.getDestination());
+        if (destination == null) {
+            throw new IllegalArgumentException("Connection has a destination with identifier " + proposed.getIdentifier()
+                + " but no component could be found in the Process Group with a corresponding identifier");
+        }
+
+        final Connection connection = flowController.createConnection(generateUuid(componentIdSeed), proposed.getName(), source, destination, proposed.getSelectedRelationships());
+        connection.setVersionedComponentId(proposed.getIdentifier());
+        addConnection(connection);
+        updateConnection(connection, proposed);
+
+        return connection;
+    }
+
+    private Connectable getConnectable(final ConnectableComponent connectableComponent) {
+        final String id = connectableComponent.getId();
+
+        switch (connectableComponent.getType()) {
+            case FUNNEL:
+                return getFunnels().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny()
+                    .orElse(null);
+            case INPUT_PORT:
+                return getInputPorts().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny()
+                    .orElse(null);
+            case OUTPUT_PORT:
+                return getOutputPorts().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny()
+                    .orElse(null);
+            case PROCESSOR:
+                return getProcessors().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny()
+                    .orElse(null);
+            case REMOTE_INPUT_PORT: {
+                final String rpgId = connectableComponent.getGroupId();
+                final Optional<RemoteProcessGroup> rpgOption = getRemoteProcessGroups().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny();
+
+                if (!rpgOption.isPresent()) {
+                    throw new IllegalArgumentException("Connection refers to a Port with ID " + id + " within Remote Process Group with ID "
+                        + rpgId + " but could not find a Remote Process Group corresponding to that ID");
+                }
+
+                final RemoteProcessGroup rpg = rpgOption.get();
+                return rpg.getInputPorts().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny()
+                    .orElse(null);
+            }
+            case REMOTE_OUTPUT_PORT: {
+                final String rpgId = connectableComponent.getGroupId();
+                final Optional<RemoteProcessGroup> rpgOption = getRemoteProcessGroups().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny();
+
+                if (!rpgOption.isPresent()) {
+                    throw new IllegalArgumentException("Connection refers to a Port with ID " + id + " within Remote Process Group with ID "
+                        + rpgId + " but could not find a Remote Process Group corresponding to that ID");
+                }
+
+                final RemoteProcessGroup rpg = rpgOption.get();
+                return rpg.getOutputPorts().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny()
+                    .orElse(null);
+            }
+        }
+
+        return null;
+    }
+
+    private void updateControllerService(final ControllerServiceNode service, final VersionedControllerService proposed) {
+        service.setAnnotationData(proposed.getAnnotationData());
+        service.setComments(proposed.getComments());
+        service.setName(proposed.getName());
+        service.setProperties(populatePropertiesMap(service.getProperties(), proposed.getProperties()));
+
+        if (!isEqual(service.getBundleCoordinate(), proposed.getBundle())) {
+            final BundleCoordinate newBundleCoordinate = toCoordinate(proposed.getBundle());
+            final List<PropertyDescriptor> descriptors = new ArrayList<>(service.getProperties().keySet());
+            final Set<URL> additionalUrls = service.getAdditionalClasspathResources(descriptors);
+            flowController.reload(service, proposed.getType(), newBundleCoordinate, additionalUrls);
+        }
+    }
+
+    private boolean isEqual(final BundleCoordinate coordinate, final Bundle bundle) {
+        if (!bundle.getGroup().equals(coordinate.getGroup())) {
+            return false;
+        }
+
+        if (!bundle.getArtifact().equals(coordinate.getId())) {
+            return false;
+        }
+
+        if (!bundle.getVersion().equals(coordinate.getVersion())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private BundleCoordinate toCoordinate(final Bundle bundle) {
+        return new BundleCoordinate(bundle.getGroup(), bundle.getArtifact(), bundle.getVersion());
+    }
+
+    private ControllerServiceNode addControllerService(final VersionedControllerService proposed, final String componentIdSeed) {
+        final String type = proposed.getType();
+        final String id = generateUuid(componentIdSeed);
+
+        final Bundle bundle = proposed.getBundle();
+        final BundleCoordinate coordinate = toCoordinate(bundle);
+        final boolean firstTimeAdded = true;
+        final Set<URL> additionalUrls = Collections.emptySet();
+
+        final ControllerServiceNode newService = flowController.createControllerService(type, id, coordinate, additionalUrls, firstTimeAdded);
+        newService.setVersionedComponentId(proposed.getIdentifier());
+
+        addControllerService(newService);
+        updateControllerService(newService, proposed);
+
+        return newService;
+    }
+
+    private void updateFunnel(final Funnel funnel, final VersionedFunnel proposed) {
+        funnel.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
+    }
+
+    private Funnel addFunnel(final VersionedFunnel proposed, final String componentIdSeed) {
+        final Funnel funnel = flowController.createFunnel(generateUuid(componentIdSeed));
+        funnel.setVersionedComponentId(proposed.getIdentifier());
+        addFunnel(funnel);
+        updateFunnel(funnel, proposed);
+
+        return funnel;
+    }
+
+    private void updatePort(final Port port, final VersionedPort proposed) {
+        port.setComments(proposed.getComments());
+        port.setName(proposed.getName());
+        port.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
+    }
+
+    private Port addInputPort(final VersionedPort proposed, final String componentIdSeed) {
+        final Port port = flowController.createLocalInputPort(generateUuid(componentIdSeed), proposed.getName());
+        port.setVersionedComponentId(proposed.getIdentifier());
+        addInputPort(port);
+        updatePort(port, proposed);
+
+        return port;
+    }
+
+    private Port addOutputPort(final VersionedPort proposed, final String componentIdSeed) {
+        final Port port = flowController.createLocalInputPort(generateUuid(componentIdSeed), proposed.getName());
+        port.setVersionedComponentId(proposed.getIdentifier());
+        addOutputPort(port);
+        updatePort(port, proposed);
+
+        return port;
+    }
+
+    private Label addLabel(final VersionedLabel proposed, final String componentIdSeed) {
+        final Label label = flowController.createLabel(generateUuid(componentIdSeed), proposed.getLabel());
+        label.setVersionedComponentId(proposed.getIdentifier());
+        addLabel(label);
+        updateLabel(label, proposed);
+
+        return label;
+    }
+
+    private void updateLabel(final Label label, final VersionedLabel proposed) {
+        label.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
+        label.setSize(new Size(proposed.getWidth(), proposed.getHeight()));
+        label.setStyle(proposed.getStyle());
+        label.setValue(proposed.getLabel());
+    }
+
+    private ProcessorNode addProcessor(final VersionedProcessor proposed, final String componentIdSeed) throws ProcessorInstantiationException {
+        final BundleCoordinate coordinate = toCoordinate(proposed.getBundle());
+        final ProcessorNode procNode = flowController.createProcessor(proposed.getType(), generateUuid(componentIdSeed), coordinate, true);
+        procNode.setVersionedComponentId(proposed.getIdentifier());
+
+        addProcessor(procNode);
+        updateProcessor(procNode, proposed);
+
+        return procNode;
+    }
+
+    private void updateProcessor(final ProcessorNode processor, final VersionedProcessor proposed) throws ProcessorInstantiationException {
+        processor.setAnnotationData(proposed.getAnnotationData());
+        processor.setBulletinLevel(LogLevel.valueOf(proposed.getBulletinLevel()));
+        processor.setComments(proposed.getComments());
+        processor.setMaxConcurrentTasks(proposed.getConcurrentlySchedulableTaskCount());
+        processor.setExecutionNode(ExecutionNode.valueOf(proposed.getExecutionNode()));
+        processor.setName(proposed.getName());
+        processor.setPenalizationPeriod(proposed.getPenaltyDuration());
+        processor.setProperties(populatePropertiesMap(processor.getProperties(), proposed.getProperties()));
+        processor.setRunDuration(proposed.getRunDurationMillis(), TimeUnit.MILLISECONDS);
+        processor.setScheduldingPeriod(proposed.getSchedulingPeriod());
+        processor.setSchedulingStrategy(SchedulingStrategy.valueOf(proposed.getSchedulingStrategy()));
+        processor.setStyle(proposed.getStyle());
+        processor.setYieldPeriod(proposed.getYieldDuration());
+        processor.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
+
+        processor.setAutoTerminatedRelationships(proposed.getAutoTerminatedRelationships().stream()
+            .map(relName -> processor.getRelationship(relName))
+            .collect(Collectors.toSet()));
+
+        if (!isEqual(processor.getBundleCoordinate(), proposed.getBundle())) {
+            final BundleCoordinate newBundleCoordinate = toCoordinate(proposed.getBundle());
+            final List<PropertyDescriptor> descriptors = new ArrayList<>(processor.getProperties().keySet());
+            final Set<URL> additionalUrls = processor.getAdditionalClasspathResources(descriptors);
+            flowController.reload(processor, proposed.getType(), newBundleCoordinate, additionalUrls);
+        }
+    }
+
+    private Map<String, String> populatePropertiesMap(final Map<PropertyDescriptor, String> currentProperties, final Map<String, String> proposedProperties) {
+        final Map<String, String> fullPropertyMap = new HashMap<>();
+        for (final PropertyDescriptor property : currentProperties.keySet()) {
+            fullPropertyMap.put(property.getName(), null);
+        }
+
+        fullPropertyMap.putAll(proposedProperties);
+        return fullPropertyMap;
+    }
+
+    private RemoteProcessGroup addRemoteProcessGroup(final VersionedRemoteProcessGroup proposed, final String componentIdSeed) {
+        final RemoteProcessGroup rpg = flowController.createRemoteProcessGroup(generateUuid(componentIdSeed), proposed.getTargetUris());
+        rpg.setVersionedComponentId(proposed.getIdentifier());
+
+        addRemoteProcessGroup(rpg);
+        updateRemoteProcessGroup(rpg, proposed);
+
+        return rpg;
+    }
+
+    private void updateRemoteProcessGroup(final RemoteProcessGroup rpg, final VersionedRemoteProcessGroup proposed) {
+        rpg.setComments(proposed.getComments());
+        rpg.setCommunicationsTimeout(proposed.getCommunicationsTimeout());
+        rpg.setInputPorts(proposed.getInputPorts().stream()
+            .map(port -> createPortDescriptor(port))
+            .collect(Collectors.toSet()));
+        rpg.setName(proposed.getName());
+        rpg.setNetworkInterface(proposed.getLocalNetworkInterface());
+        rpg.setOutputPorts(proposed.getOutputPorts().stream()
+            .map(port -> createPortDescriptor(port))
+            .collect(Collectors.toSet()));
+        rpg.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
+        rpg.setProxyHost(proposed.getProxyHost());
+        rpg.setProxyPort(proposed.getProxyPort());
+        rpg.setProxyUser(proposed.getProxyUser());
+        rpg.setTransportProtocol(SiteToSiteTransportProtocol.valueOf(proposed.getTransportProtocol()));
+        rpg.setYieldDuration(proposed.getYieldDuration());
+    }
+
+    private RemoteProcessGroupPortDescriptor createPortDescriptor(final VersionedRemoteGroupPort proposed) {
+        final StandardRemoteProcessGroupPortDescriptor descriptor = new StandardRemoteProcessGroupPortDescriptor();
+        descriptor.setVersionedComponentId(proposed.getIdentifier());
+        descriptor.setBatchCount(proposed.getBatchSize().getCount());
+        descriptor.setBatchDuration(proposed.getBatchSize().getDuration());
+        descriptor.setBatchSize(proposed.getBatchSize().getSize());
+        descriptor.setComments(proposed.getComments());
+        descriptor.setConcurrentlySchedulableTaskCount(proposed.getConcurrentlySchedulableTaskCount());
+        descriptor.setGroupId(proposed.getGroupId());
+        descriptor.setId(UUID.randomUUID().toString()); // TODO: Need to address this issue of port id's
+        descriptor.setName(proposed.getName());
+        descriptor.setUseCompression(proposed.isUseCompression());
+        return descriptor;
+    }
+
+
+    public Optional<Boolean> isModified() {
+        final StandardVersionControlInformation vci = versionControlInfo.get();
+
+        // If this group is not under version control, then we need to notify the parent
+        // group (if any) that a modification has taken place. Otherwise, we need to
+        // compare the current the flow with the 'versioned snapshot' of the flow in order
+        // to determine if the flows are different.
+        // We cannot simply say 'if something changed then this flow is different than the versioned snapshot'
+        // because if we do this, and a user adds a processor then subsequently removes it, then the logic would
+        // say that the flow is modified. There would be no way to ever go back to the flow not being modified.
+        // So we have to perform a diff of the flows and see if they are the same.
+        if (vci == null) {
+            return Optional.of(Boolean.FALSE);
+        }
+
+        if (vci.getFlowSnapshot() == null) {
+            // we haven't retrieved the flow from the Flow Registry yet, so we don't know if it's been modified.
+            // As a result, we will just return an empty optional
+            return Optional.empty();
+        }
+
+        final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
+        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient());
+
+        final ComparableDataFlow currentFlow = new ComparableDataFlow() {
+            @Override
+            public VersionedProcessGroup getContents() {
+                return versionedGroup;
+            }
+
+            @Override
+            public String getName() {
+                return "Local Flow";
+            }
+        };
+
+        final ComparableDataFlow snapshotFlow = new ComparableDataFlow() {
+            @Override
+            public VersionedProcessGroup getContents() {
+                return vci.getFlowSnapshot();
+            }
+
+            @Override
+            public String getName() {
+                return "Flow Under Version Control";
+            }
+        };
+
+        final FlowComparator flowComparator = new StandardFlowComparator(currentFlow, snapshotFlow);
+        final FlowComparison comparison = flowComparator.compare();
+        final Set<FlowDifference> differences = comparison.getDifferences();
+        final boolean modified = differences.stream()
+            .filter(diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED)
+            .filter(diff -> diff.getDifferenceType() != DifferenceType.STYLE_CHANGED)
+            .findAny()
+            .isPresent();
+
+        LOG.debug("There are {} differences between this flow and the versioned snapshot of this flow: {}", differences.size(), differences);
+        return Optional.of(modified);
+    }
+
+
+    @Override
+    public void verifyCanUpdate(final VersionedFlowSnapshot updatedFlow, final boolean verifyConnectionRemoval, final boolean verifyNotDirty) {
+        readLock.lock();
+        try {
+            final VersionControlInformation versionControlInfo = getVersionControlInformation();
+            if (versionControlInfo == null) {
+                throw new IllegalStateException("Cannot update the Version of the flow for " + this
+                    + " because the Process Group is not currently under Version Control");
+            }
+
+            if (!versionControlInfo.getFlowIdentifier().equals(updatedFlow.getSnapshotMetadata().getFlowIdentifier())) {
+                throw new IllegalStateException(this + " is under version control but the given flow does not match the flow that this Process Group is synchronized with");
+            }
+
+            if (verifyNotDirty) {
+                final Optional<Boolean> modifiedOption = versionControlInfo.getModified();
+                if (!modifiedOption.isPresent()) {
+                    throw new IllegalStateException(this + " cannot be updated to a different version of the flow because the local flow "
+                        + "has not yet been synchronized with the Flow Registry. The Process Group must be"
+                        + " synched with the Flow Registry before continuing. This will happen periodically in the background, so please try the request again later");
+                }
+
+                if (Boolean.TRUE.equals(modifiedOption.get())) {
+                    throw new IllegalStateException("Cannot change the Version of the flow for " + this
+                        + " because the Process Group has been modified since it was last synchronized with the Flow Registry. The Process Group must be"
+                        + " restored to its original form before changing the version");
+                }
+            }
+
+            final VersionedProcessGroup flowContents = updatedFlow.getFlowContents();
+            if (verifyConnectionRemoval) {
+                // Determine which Connections have been removed.
+                final Map<String, Connection> removedConnectionByVersionedId = new HashMap<>();
+                findAllConnections().stream()
+                    .filter(conn -> conn.getVersionedComponentId().isPresent())
+                    .forEach(conn -> removedConnectionByVersionedId.put(conn.getVersionedComponentId().get(), conn));
+
+                final Set<String> proposedFlowConnectionIds = new HashSet<>();
+                findAllConnectionIds(flowContents, proposedFlowConnectionIds);
+
+                for (final String proposedConnectionId : proposedFlowConnectionIds) {
+                    removedConnectionByVersionedId.remove(proposedConnectionId);
+                }
+
+                // If any connection that was removed has data in it, throw an IllegalStateException
+                for (final Connection connection : removedConnectionByVersionedId.values()) {
+                    final FlowFileQueue flowFileQueue = connection.getFlowFileQueue();
+                    if (!flowFileQueue.isEmpty()) {
+                        throw new IllegalStateException(this + " cannot be updated to the proposed version of the flow because the "
+                            + "proposed version does not contain "
+                            + connection + " and the connection currently has data in the queue.");
+                    }
+                }
+            }
+
+            // Determine which input ports were removed from this process group
+            final Map<String, Port> removedInputPortsByVersionId = new HashMap<>();
+            getInputPorts().stream()
+                .filter(port -> port.getVersionedComponentId().isPresent())
+                .forEach(port -> removedInputPortsByVersionId.put(port.getVersionedComponentId().get(), port));
+            flowContents.getInputPorts().stream()
+                .map(VersionedPort::getIdentifier)
+                .forEach(id -> removedInputPortsByVersionId.remove(id));
+
+            // Ensure that there are no incoming connections for any Input Port that was removed.
+            for (final Port inputPort : removedInputPortsByVersionId.values()) {
+                final List<Connection> incomingConnections = inputPort.getIncomingConnections();
+                if (!incomingConnections.isEmpty()) {
+                    throw new IllegalStateException(this + " cannot be updated to the proposed version of the flow because the proposed version does not contain the Input Port "
+                        + inputPort + " and the Input Port currently has an incoming connections");
+                }
+            }
+
+            // Determine which output ports were removed from this process group
+            final Map<String, Port> removedOutputPortsByVersionId = new HashMap<>();
+            getOutputPorts().stream()
+                .filter(port -> port.getVersionedComponentId().isPresent())
+                .forEach(port -> removedOutputPortsByVersionId.put(port.getVersionedComponentId().get(), port));
+            flowContents.getOutputPorts().stream()
+                .map(VersionedPort::getIdentifier)
+                .forEach(id -> removedOutputPortsByVersionId.remove(id));
+
+            // Ensure that there are no outgoing connections for any Output Port that was removed.
+            for (final Port outputPort : removedOutputPortsByVersionId.values()) {
+                final Set<Connection> outgoingConnections = outputPort.getConnections();
+                if (!outgoingConnections.isEmpty()) {
+                    throw new IllegalStateException(this + " cannot be updated to the proposed version of the flow because the proposed version does not contain the Output Port "
+                        + outputPort + " and the Output Port currently has an outgoing connections");
+                }
+            }
+
+            // Find any Process Groups that may have been deleted. If we find any Process Group that was deleted, and that Process Group
+            // has Templates, then we fail because the Templates have to be removed first.
+            final Map<String, VersionedProcessGroup> proposedProcessGroups = new HashMap<>();
+            findAllProcessGroups(updatedFlow.getFlowContents(), proposedProcessGroups);
+
+            for (final ProcessGroup childGroup : findAllProcessGroups()) {
+                if (childGroup.getTemplates().isEmpty()) {
+                    continue;
+                }
+
+                final Optional<String> versionedIdOption = childGroup.getVersionedComponentId();
+                if (!versionedIdOption.isPresent()) {
+                    continue;
+                }
+
+                final String versionedId = versionedIdOption.get();
+                if (!proposedProcessGroups.containsKey(versionedId)) {
+                    // Process Group was removed.
+                    throw new IllegalStateException(this + " cannot be updated to the proposed version of the flow because the child " + childGroup
+                        + " that exists locally has one or more Templates, and the proposed flow does not contain this Process Group. "
+                        + "A Process Group cannot be deleted while it contains Templates. Please remove the Templates before attempting to chnage the version of the flow.");
+                }
+            }
+
+
+            // Ensure that all Processors are instantiate-able.
+            final Map<String, VersionedProcessor> proposedProcessors = new HashMap<>();
+            findAllProcessors(updatedFlow.getFlowContents(), proposedProcessors);
+
+            findAllProcessors().stream()
+                .filter(proc -> proc.getVersionedComponentId().isPresent())
+                .forEach(proc -> proposedProcessors.remove(proc.getVersionedComponentId().get()));
+
+            for (final VersionedProcessor processorToAdd : proposedProcessors.values()) {
+                final BundleCoordinate coordinate = toCoordinate(processorToAdd.getBundle());
+                try {
+                    flowController.createProcessor(processorToAdd.getType(), UUID.randomUUID().toString(), coordinate, false);
+                } catch (Exception e) {
+                    throw new IllegalArgumentException("Unable to create Processor of type " + processorToAdd.getType(), e);
+                }
+            }
+
+            // Ensure that all Controller Services are instantiate-able.
+            final Map<String, VersionedControllerService> proposedServices = new HashMap<>();
+            findAllControllerServices(updatedFlow.getFlowContents(), proposedServices);
+
+            findAllControllerServices().stream()
+                .filter(service -> service.getVersionedComponentId().isPresent())
+                .forEach(service -> proposedServices.remove(service.getVersionedComponentId().get()));
+
+            for (final VersionedControllerService serviceToAdd : proposedServices.values()) {
+                final BundleCoordinate coordinate = toCoordinate(serviceToAdd.getBundle());
+                try {
+                    flowController.createControllerService(serviceToAdd.getType(), UUID.randomUUID().toString(), coordinate, Collections.emptySet(), false);
+                } catch (Exception e) {
+                    throw new IllegalArgumentException("Unable to create Controller Service of type " + serviceToAdd.getType(), e);
+                }
+            }
+
+            // Ensure that all Prioritizers are instantiate-able.
+            final Map<String, VersionedConnection> proposedConnections = new HashMap<>();
+            findAllConnections(updatedFlow.getFlowContents(), proposedConnections);
+
+            findAllConnections().stream()
+                .filter(conn -> conn.getVersionedComponentId().isPresent())
+                .forEach(conn -> proposedConnections.remove(conn.getVersionedComponentId().get()));
+
+            for (final VersionedConnection connectionToAdd : proposedConnections.values()) {
+                for (final String prioritizerType : connectionToAdd.getPrioritizers()) {
+                    try {
+                        flowController.createPrioritizer(prioritizerType);
+                    } catch (Exception e) {
+                        throw new IllegalArgumentException("Unable to create Prioritizer of type " + prioritizerType, e);
+                    }
+                }
+            }
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    private void findAllConnectionIds(final VersionedProcessGroup group, final Set<String> ids) {
+        for (final VersionedConnection connection : group.getConnections()) {
+            ids.add(connection.getIdentifier());
+        }
+
+        for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
+            findAllConnectionIds(childGroup, ids);
+        }
+    }
+
+    private void findAllProcessors(final VersionedProcessGroup group, final Map<String, VersionedProcessor> map) {
+        for (final VersionedProcessor processor : group.getProcessors()) {
+            map.put(processor.getIdentifier(), processor);
+        }
+
+        for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
+            findAllProcessors(childGroup, map);
+        }
+    }
+
+    private void findAllControllerServices(final VersionedProcessGroup group, final Map<String, VersionedControllerService> map) {
+        for (final VersionedControllerService service : group.getControllerServices()) {
+            map.put(service.getIdentifier(), service);
+        }
+
+        for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
+            findAllControllerServices(childGroup, map);
+        }
+    }
+
+    private void findAllConnections(final VersionedProcessGroup group, final Map<String, VersionedConnection> map) {
+        for (final VersionedConnection connection : group.getConnections()) {
+            map.put(connection.getIdentifier(), connection);
+        }
+
+        for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
+            findAllConnections(childGroup, map);
+        }
+    }
+
+    private void findAllProcessGroups(final VersionedProcessGroup group, final Map<String, VersionedProcessGroup> map) {
+        map.put(group.getIdentifier(), group);
+
+        for (final VersionedProcessGroup child : group.getProcessGroups()) {
+            findAllProcessGroups(child, map);
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
new file mode 100644
index 0000000..22ba50b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
@@ -0,0 +1,404 @@
+/*
+ * 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.registry.flow;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.codehaus.jackson.JsonFactory;
+import org.codehaus.jackson.JsonGenerator;
+import org.codehaus.jackson.JsonParser;
+import org.codehaus.jackson.map.DeserializationConfig;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.util.DefaultPrettyPrinter;
+
+/**
+ * A simple file-based implementation of a Flow Registry Client. Rather than interacting
+ * with an actual Flow Registry, this implementation simply reads flows from disk and writes
+ * them to disk. It is not meant for any production use but is available for testing purposes.
+ */
+public class FileBasedFlowRegistryClient implements FlowRegistryClient, FlowRegistry {
+    private final File directory;
+    private final Map<String, Set<String>> flowNamesByBucket = new HashMap<>();
+    private final JsonFactory jsonFactory = new JsonFactory();
+
+    public FileBasedFlowRegistryClient(final File directory) throws IOException {
+        if (!directory.exists() && !directory.mkdirs()) {
+            throw new IOException("Could not access or create directory " + directory.getAbsolutePath() + " for Flow Registry");
+        }
+
+        this.directory = directory;
+        recoverBuckets();
+    }
+
+    private void recoverBuckets() throws IOException {
+        final File[] bucketDirs = directory.listFiles();
+        if (bucketDirs == null) {
+            throw new IOException("Could not get listing of directory " + directory);
+        }
+
+        for (final File bucketDir : bucketDirs) {
+            final File[] flowDirs = bucketDir.listFiles();
+            if (flowDirs == null) {
+                throw new IOException("Could not get listing of directory " + bucketDir);
+            }
+
+            final Set<String> flowNames = new HashSet<>();
+            for (final File flowDir : flowDirs) {
+                final File propsFile = new File(flowDir, "flow.properties");
+                if (!propsFile.exists()) {
+                    continue;
+                }
+
+                final Properties properties = new Properties();
+                try (final InputStream in = new FileInputStream(propsFile)) {
+                    properties.load(in);
+                }
+
+                final String flowName = properties.getProperty("name");
+                if (flowName == null) {
+                    continue;
+                }
+
+                flowNames.add(flowName);
+            }
+
+            if (!flowNames.isEmpty()) {
+                flowNamesByBucket.put(bucketDir.getName(), flowNames);
+            }
+        }
+    }
+
+    @Override
+    public FlowRegistry getFlowRegistry(final String registryId) {
+        if (!"default".equals(registryId)) {
+            return null;
+        }
+
+        return this;
+    }
+
+    @Override
+    public String getURL() {
+        return directory.toURI().toString();
+    }
+
+    @Override
+    public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, UnknownResourceException {
+        Objects.requireNonNull(flow);
+        Objects.requireNonNull(flow.getBucketIdentifier());
+        Objects.requireNonNull(flow.getName());
+
+        // Verify that bucket exists
+        final File bucketDir = new File(directory, flow.getBucketIdentifier());
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
+        }
+
+        // Verify that there is no flow with the same name in that bucket
+        final Set<String> flowNames = flowNamesByBucket.get(flow.getBucketIdentifier());
+        if (flowNames != null && flowNames.contains(flow.getName())) {
+            throw new IllegalArgumentException("Flow with name '" + flow.getName() + "' already exists for Bucket with ID " + flow.getBucketIdentifier());
+        }
+
+        final String flowIdentifier = UUID.randomUUID().toString();
+        final File flowDir = new File(bucketDir, flowIdentifier);
+        if (!flowDir.mkdirs()) {
+            throw new IOException("Failed to create directory " + flowDir + " for new Flow");
+        }
+
+        final File propertiesFile = new File(flowDir, "flow.properties");
+
+        final Properties flowProperties = new Properties();
+        flowProperties.setProperty("name", flow.getName());
+        flowProperties.setProperty("created", String.valueOf(flow.getCreatedTimestamp()));
+        flowProperties.setProperty("description", flow.getDescription());
+        flowProperties.setProperty("lastModified", String.valueOf(flow.getModifiedTimestamp()));
+
+        try (final OutputStream out = new FileOutputStream(propertiesFile)) {
+            flowProperties.store(out, null);
+        }
+
+        final VersionedFlow response = new VersionedFlow();
+        response.setBucketIdentifier(flow.getBucketIdentifier());
+        response.setCreatedTimestamp(flow.getCreatedTimestamp());
+        response.setDescription(flow.getDescription());
+        response.setIdentifier(flowIdentifier);
+        response.setModifiedTimestamp(flow.getModifiedTimestamp());
+        response.setName(flow.getName());
+
+        return response;
+    }
+
+    @Override
+    public synchronized VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments)
+            throws IOException, UnknownResourceException {
+        Objects.requireNonNull(flow);
+        Objects.requireNonNull(flow.getBucketIdentifier());
+        Objects.requireNonNull(flow.getName());
+        Objects.requireNonNull(snapshot);
+
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, flow.getBucketIdentifier());
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flow.getIdentifier());
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flow.getIdentifier() + " exists for Bucket with ID " + flow.getBucketIdentifier());
+        }
+
+        final File[] versionDirs = flowDir.listFiles();
+        if (versionDirs == null) {
+            throw new IOException("Unable to perform listing of directory " + flowDir);
+        }
+
+        int maxVersion = 0;
+        for (final File versionDir : versionDirs) {
+            final String versionName = versionDir.getName();
+
+            final int version;
+            try {
+                version = Integer.parseInt(versionName);
+            } catch (final NumberFormatException nfe) {
+                continue;
+            }
+
+            if (version > maxVersion) {
+                maxVersion = version;
+            }
+        }
+
+        final int snapshotVersion = maxVersion + 1;
+        final File snapshotDir = new File(flowDir, String.valueOf(snapshotVersion));
+        if (!snapshotDir.mkdir()) {
+            throw new IOException("Could not create directory " + snapshotDir);
+        }
+
+        final File contentsFile = new File(snapshotDir, "flow.xml");
+
+        try (final OutputStream out = new FileOutputStream(contentsFile);
+            final JsonGenerator generator = jsonFactory.createJsonGenerator(out)) {
+            generator.setCodec(new ObjectMapper());
+            generator.setPrettyPrinter(new DefaultPrettyPrinter());
+            generator.writeObject(snapshot);
+        }
+
+        final Properties snapshotProperties = new Properties();
+        snapshotProperties.setProperty("comments", comments);
+        snapshotProperties.setProperty("name", flow.getName());
+        final File snapshotPropsFile = new File(snapshotDir, "snapshot.properties");
+        try (final OutputStream out = new FileOutputStream(snapshotPropsFile)) {
+            snapshotProperties.store(out, null);
+        }
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier());
+        snapshotMetadata.setComments(comments);
+        snapshotMetadata.setFlowIdentifier(flow.getIdentifier());
+        snapshotMetadata.setFlowName(flow.getName());
+        snapshotMetadata.setTimestamp(System.currentTimeMillis());
+        snapshotMetadata.setVersion(snapshotVersion);
+
+        final VersionedFlowSnapshot response = new VersionedFlowSnapshot();
+        response.setSnapshotMetadata(snapshotMetadata);
+        response.setFlowContents(snapshot);
+        return response;
+    }
+
+    @Override
+    public Set<String> getRegistryIdentifiers() {
+        return Collections.singleton("default");
+    }
+
+    @Override
+    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, bucketId);
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flowId);
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + bucketId);
+        }
+
+        final File[] versionDirs = flowDir.listFiles();
+        if (versionDirs == null) {
+            throw new IOException("Unable to perform listing of directory " + flowDir);
+        }
+
+        int maxVersion = 0;
+        for (final File versionDir : versionDirs) {
+            final String versionName = versionDir.getName();
+
+            final int version;
+            try {
+                version = Integer.parseInt(versionName);
+            } catch (final NumberFormatException nfe) {
+                continue;
+            }
+
+            if (version > maxVersion) {
+                maxVersion = version;
+            }
+        }
+
+        return maxVersion;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, int version) throws IOException, UnknownResourceException {
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, bucketId);
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flowId);
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
+        }
+
+        final File versionDir = new File(flowDir, String.valueOf(version));
+        if (!versionDir.exists()) {
+            throw new UnknownResourceException("Flow with ID " + flowId + " in Bucket with ID " + bucketId + " does not contain a snapshot with version " + version);
+        }
+
+        final File contentsFile = new File(versionDir, "flow.xml");
+
+        final VersionedProcessGroup processGroup;
+        try (final JsonParser parser = jsonFactory.createJsonParser(contentsFile)) {
+            final ObjectMapper mapper = new ObjectMapper();
+            mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+            parser.setCodec(mapper);
+            processGroup = parser.readValueAs(VersionedProcessGroup.class);
+        }
+
+        final Properties properties = new Properties();
+        final File snapshotPropsFile = new File(versionDir, "snapshot.properties");
+        try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
+            properties.load(in);
+        }
+
+        final String comments = properties.getProperty("comments");
+        final String flowName = properties.getProperty("name");
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setBucketIdentifier(bucketId);
+        snapshotMetadata.setComments(comments);
+        snapshotMetadata.setFlowIdentifier(flowId);
+        snapshotMetadata.setFlowName(flowName);
+        snapshotMetadata.setTimestamp(System.currentTimeMillis());
+        snapshotMetadata.setVersion(version);
+
+        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
+        snapshot.setFlowContents(processGroup);
+        snapshot.setSnapshotMetadata(snapshotMetadata);
+
+        return snapshot;
+    }
+
+    @Override
+    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, bucketId);
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flowId);
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
+        }
+
+        final File flowPropsFile = new File(flowDir, "flow.properties");
+        final Properties flowProperties = new Properties();
+        try (final InputStream in = new FileInputStream(flowPropsFile)) {
+            flowProperties.load(in);
+        }
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setCreatedTimestamp(Long.parseLong(flowProperties.getProperty("created")));
+        flow.setDescription(flowProperties.getProperty("description"));
+        flow.setIdentifier(flowId);
+        flow.setModifiedTimestamp(flowDir.lastModified());
+        flow.setName(flowProperties.getProperty("name"));
+
+        final Comparator<VersionedFlowSnapshotMetadata> versionComparator = (a, b) -> Integer.compare(a.getVersion(), b.getVersion());
+
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshotMetadataSet = new TreeSet<>(versionComparator);
+        flow.setSnapshotMetadata(snapshotMetadataSet);
+
+        final File[] versionDirs = flowDir.listFiles();
+        for (final File file : versionDirs) {
+            if (!file.isDirectory()) {
+                continue;
+            }
+
+            int version;
+            try {
+                version = Integer.parseInt(file.getName());
+            } catch (final NumberFormatException nfe) {
+                // not a version. skip.
+                continue;
+            }
+
+            final File snapshotPropsFile = new File(file, "snapshot.properties");
+            final Properties snapshotProperties = new Properties();
+            try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
+                snapshotProperties.load(in);
+            }
+
+            final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
+            metadata.setBucketIdentifier(bucketId);
+            metadata.setComments(snapshotProperties.getProperty("comments"));
+            metadata.setFlowIdentifier(flowId);
+            metadata.setFlowName(snapshotProperties.getProperty("name"));
+            metadata.setTimestamp(file.lastModified());
+            metadata.setVersion(version);
+
+            snapshotMetadataSet.add(metadata);
+        }
+
+        return flow;
+    }
+}


[40/50] nifi git commit: NIFI-4436: Fixed bug that causes a deadlock when changing version of a PG. Before this patch, an update would obtain a write lock and then recurse downward through the child groups, obtaining write locks to update variable regist

Posted by bb...@apache.org.
NIFI-4436: Fixed bug that causes a deadlock when changing version of a PG. Before this patch, an update would obtain a write lock and then recurse downward through the child groups, obtaining write locks to update variable registries. At the same time, if a Processor is obtaining a Controller Service, it will obtain a Read Lock on the Process Group and then recurse upward through the ancestors, obtaining Read Lock. If the timing is right, we can have a group obtain a read lock, then try to obtain its parent's Read Lock. At the same time, an update to the group could hold the Write Lock on the Process Group and attempt to obtain a Write Lock on child (where the Processor lives), resulting in a deadlock.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/0127b026
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/0127b026
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/0127b026

Branch: refs/heads/master
Commit: 0127b02617530491a1a55aa72395cee583083956
Parents: c5b0931
Author: Mark Payne <ma...@hotmail.com>
Authored: Sat Dec 30 14:16:26 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:56 2018 -0500

----------------------------------------------------------------------
 .../apache/nifi/groups/StandardProcessGroup.java  | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/0127b026/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 5d5d0f4..bc5ef29 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -2028,19 +2028,23 @@ public final class StandardProcessGroup implements ProcessGroup {
 
     @Override
     public Set<ControllerServiceNode> getControllerServices(final boolean recursive) {
+        final Set<ControllerServiceNode> services = new HashSet<>();
+
         readLock.lock();
         try {
-            final Set<ControllerServiceNode> services = new HashSet<>();
             services.addAll(controllerServices.values());
-
-            if (recursive && parent.get() != null) {
-                services.addAll(parent.get().getControllerServices(true));
-            }
-
-            return services;
         } finally {
             readLock.unlock();
         }
+
+        if (recursive) {
+            final ProcessGroup parentGroup = parent.get();
+            if (parentGroup != null) {
+                services.addAll(parentGroup.getControllerServices(true));
+            }
+        }
+
+        return services;
     }
 
     @Override


[42/50] nifi git commit: NIFI-4436: Bug fixes

Posted by bb...@apache.org.
NIFI-4436: Bug fixes

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/416b8614
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/416b8614
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/416b8614

Branch: refs/heads/master
Commit: 416b86145f58210baf548d422865068faf7acf39
Parents: f48808b
Author: Mark Payne <ma...@hotmail.com>
Authored: Wed Dec 13 13:57:59 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:56 2018 -0500

----------------------------------------------------------------------
 .../flow/StandardFlowRegistryClient.java        | 22 ++++++++++----------
 1 file changed, 11 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/416b8614/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
index 4f98a2b..754646b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.registry.flow;
 
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -58,8 +59,17 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
 
     @Override
     public FlowRegistry addFlowRegistry(final String registryId, final String registryName, final String registryUrl, final String description) {
-        final URI uri = URI.create(registryUrl);
+        final URI uri;
+        try {
+            uri = new URI(registryUrl);
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("The given Registry URL is not valid: " + registryUrl);
+        }
+
         final String uriScheme = uri.getScheme();
+        if (uriScheme == null) {
+            throw new IllegalArgumentException("The given Registry URL is not valid: " + registryUrl);
+        }
 
         final FlowRegistry registry;
         if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
@@ -72,16 +82,6 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
 
             registry = new RestBasedFlowRegistry(this, registryId, registryUrl, sslContext, registryName);
             registry.setDescription(description);
-        } else if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
-            final SSLContext sslContext = SslContextFactory.createSslContext(nifiProperties, false);
-            if (sslContext == null && uriScheme.equalsIgnoreCase("https")) {
-                throw new IllegalStateException("Failed to create Flow Registry for URI " + registryUrl
-                    + " because this NiFi is not configured with a Keystore/Truststore, so it is not capable of communicating with a secure Registry. "
-                    + "Please populate NiFi's Keystore/Truststore properties or connect to a NiFi Registry over http instead of https.");
-            }
-
-            registry = new RestBasedFlowRegistry(this, registryId, registryUrl, sslContext, registryName);
-            registry.setDescription(description);
         } else {
             throw new IllegalArgumentException("Cannot create Flow Registry with URI of " + registryUrl
                 + " because there are no known implementations of Flow Registries that can handle URIs of scheme " + uriScheme);


[34/50] nifi git commit: NIFI-4436: - Code clean up. - Improved error handling. - Minor UX improvements. - Always showing Process Group state to complement the aggregation counts. - Adding the Process Group state to the top status bar.

Posted by bb...@apache.org.
NIFI-4436:
- Code clean up.
- Improved error handling.
- Minor UX improvements.
- Always showing Process Group state to complement the aggregation counts.
- Adding the Process Group state to the top status bar.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/1266235c
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/1266235c
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/1266235c

Branch: refs/heads/master
Commit: 1266235c00345f222b34480be00ba0db951e2077
Parents: fe8b30b
Author: Matt Gilman <ma...@gmail.com>
Authored: Mon Dec 11 15:14:44 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:55 2018 -0500

----------------------------------------------------------------------
 .../web/api/dto/status/ControllerStatusDTO.java |  56 ++++
 .../web/api/entity/FlowBreadcrumbEntity.java    |  12 +
 .../nifi/web/api/entity/ProcessGroupEntity.java |  69 ++++-
 .../apache/nifi/web/api/ControllerResource.java |  41 ++-
 .../apache/nifi/web/api/VersionsResource.java   |  28 +-
 .../apache/nifi/web/api/dto/EntityFactory.java  |  17 ++
 .../nifi/web/controller/ControllerFacade.java   |   5 +
 .../WEB-INF/partials/canvas/flow-status.jsp     |   7 +
 .../canvas/new-process-group-dialog.jsp         |   5 +-
 .../src/main/webapp/css/common-ui.css           |   2 +-
 .../src/main/webapp/css/navigation.css          |  22 +-
 .../controllers/nf-ng-breadcrumbs-controller.js |   8 +-
 .../nf-ng-canvas-flow-status-controller.js      |  50 +++
 .../main/webapp/js/nf/canvas/nf-flow-version.js |  56 ++--
 .../src/main/webapp/js/nf/canvas/nf-port.js     |   5 +-
 .../webapp/js/nf/canvas/nf-process-group.js     | 301 +++++++++----------
 .../main/webapp/js/nf/canvas/nf-processor.js    |   5 +-
 .../js/nf/canvas/nf-remote-process-group.js     |  19 +-
 18 files changed, 456 insertions(+), 252 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ControllerStatusDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ControllerStatusDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ControllerStatusDTO.java
index eb31597..cddf85e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ControllerStatusDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ControllerStatusDTO.java
@@ -38,6 +38,12 @@ public class ControllerStatusDTO implements Cloneable {
     private Integer activeRemotePortCount = 0;
     private Integer inactiveRemotePortCount = 0;
 
+    private Integer upToDateCount;
+    private Integer locallyModifiedCount;
+    private Integer staleCount;
+    private Integer locallyModifiedAndStaleCount;
+    private Integer syncFailureCount;
+
     /**
      * The active thread count.
      *
@@ -154,6 +160,51 @@ public class ControllerStatusDTO implements Cloneable {
         this.bytesQueued = bytesQueued;
     }
 
+    @ApiModelProperty("The number of up to date versioned process groups in the NiFi.")
+    public Integer getUpToDateCount() {
+        return upToDateCount;
+    }
+
+    public void setUpToDateCount(Integer upToDateCount) {
+        this.upToDateCount = upToDateCount;
+    }
+
+    @ApiModelProperty("The number of locally modified versioned process groups in the NiFi.")
+    public Integer getLocallyModifiedCount() {
+        return locallyModifiedCount;
+    }
+
+    public void setLocallyModifiedCount(Integer locallyModifiedCount) {
+        this.locallyModifiedCount = locallyModifiedCount;
+    }
+
+    @ApiModelProperty("The number of stale versioned process groups in the NiFi.")
+    public Integer getStaleCount() {
+        return staleCount;
+    }
+
+    public void setStaleCount(Integer staleCount) {
+        this.staleCount = staleCount;
+    }
+
+    @ApiModelProperty("The number of locally modified and stale versioned process groups in the NiFi.")
+    public Integer getLocallyModifiedAndStaleCount() {
+        return locallyModifiedAndStaleCount;
+    }
+
+    public void setLocallyModifiedAndStaleCount(Integer locallyModifiedAndStaleCount) {
+        this.locallyModifiedAndStaleCount = locallyModifiedAndStaleCount;
+    }
+
+    @ApiModelProperty("The number of versioned process groups in the NiFi that are unable to sync to a registry.")
+    public Integer getSyncFailureCount() {
+        return syncFailureCount;
+    }
+
+    public void setSyncFailureCount(Integer syncFailureCount) {
+        this.syncFailureCount = syncFailureCount;
+    }
+
     @Override
     public ControllerStatusDTO clone() {
         final ControllerStatusDTO other = new ControllerStatusDTO();
@@ -167,6 +218,11 @@ public class ControllerStatusDTO implements Cloneable {
         other.setDisabledCount(getDisabledCount());
         other.setActiveRemotePortCount(getActiveRemotePortCount());
         other.setInactiveRemotePortCount(getInactiveRemotePortCount());
+        other.setUpToDateCount(getUpToDateCount());
+        other.setLocallyModifiedCount(getLocallyModifiedCount());
+        other.setStaleCount(getStaleCount());
+        other.setLocallyModifiedAndStaleCount(getLocallyModifiedAndStaleCount());
+        other.setStaleCount(getStaleCount());
         return other;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
index b97b9b3..21f9742 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
@@ -30,6 +30,7 @@ public class FlowBreadcrumbEntity extends Entity {
 
     private String id;
     private PermissionsDTO permissions;
+    private String state;
     private FlowBreadcrumbDTO breadcrumb;
     private FlowBreadcrumbEntity parentBreadcrumb;
 
@@ -96,4 +97,15 @@ public class FlowBreadcrumbEntity extends Entity {
     public void setParentBreadcrumb(FlowBreadcrumbEntity parentBreadcrumb) {
         this.parentBreadcrumb = parentBreadcrumb;
     }
+
+    @ApiModelProperty(readOnly = true,
+            value = "The current state of the Process Group, as it relates to the Versioned Flow",
+            allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
index 1e2a4b4..fe8d2d6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
@@ -16,13 +16,12 @@
  */
 package org.apache.nifi.web.api.entity;
 
-import javax.xml.bind.annotation.XmlRootElement;
-
+import io.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
 
-import io.swagger.annotations.ApiModelProperty;
+import javax.xml.bind.annotation.XmlRootElement;
 
 /**
  * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a ProcessGroupDTO.
@@ -41,6 +40,14 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
     private Integer activeRemotePortCount;
     private Integer inactiveRemotePortCount;
 
+    private String state;
+
+    private Integer upToDateCount;
+    private Integer locallyModifiedCount;
+    private Integer staleCount;
+    private Integer locallyModifiedAndStaleCount;
+    private Integer syncFailureCount;
+
     private Integer inputPortCount;
     private Integer outputPortCount;
 
@@ -193,4 +200,60 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
     public void setVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot) {
         this.versionedFlowSnapshot = versionedFlowSnapshot;
     }
+
+    @ApiModelProperty(readOnly = true,
+            value = "The current state of the Process Group, as it relates to the Versioned Flow",
+            allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    @ApiModelProperty("The number of up to date versioned process groups in the process group.")
+    public Integer getUpToDateCount() {
+        return upToDateCount;
+    }
+
+    public void setUpToDateCount(Integer upToDateCount) {
+        this.upToDateCount = upToDateCount;
+    }
+
+    @ApiModelProperty("The number of locally modified versioned process groups in the process group.")
+    public Integer getLocallyModifiedCount() {
+        return locallyModifiedCount;
+    }
+
+    public void setLocallyModifiedCount(Integer locallyModifiedCount) {
+        this.locallyModifiedCount = locallyModifiedCount;
+    }
+
+    @ApiModelProperty("The number of stale versioned process groups in the process group.")
+    public Integer getStaleCount() {
+        return staleCount;
+    }
+
+    public void setStaleCount(Integer staleCount) {
+        this.staleCount = staleCount;
+    }
+
+    @ApiModelProperty("The number of locally modified and stale versioned process groups in the process group.")
+    public Integer getLocallyModifiedAndStaleCount() {
+        return locallyModifiedAndStaleCount;
+    }
+
+    public void setLocallyModifiedAndStaleCount(Integer locallyModifiedAndStaleCount) {
+        this.locallyModifiedAndStaleCount = locallyModifiedAndStaleCount;
+    }
+
+    @ApiModelProperty("The number of versioned process groups in the process group that are unable to sync to a registry.")
+    public Integer getSyncFailureCount() {
+        return syncFailureCount;
+    }
+
+    public void setSyncFailureCount(Integer syncFailureCount) {
+        this.syncFailureCount = syncFailureCount;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
index 04c0efa..c5c40be 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
@@ -238,7 +238,6 @@ public class ControllerResource extends ApplicationResource {
                     @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
             }
     )
-
     public Response createReportingTask(
             @Context final HttpServletRequest httpServletRequest,
             @ApiParam(
@@ -383,11 +382,19 @@ public class ControllerResource extends ApplicationResource {
             throw new IllegalArgumentException("A revision of 0 must be specified when creating a new Registry.");
         }
 
-        final RegistryDTO requestReportingTask = requestRegistryClientEntity.getComponent();
-        if (requestReportingTask.getId() != null) {
+        final RegistryDTO requestRegistryClient = requestRegistryClientEntity.getComponent();
+        if (requestRegistryClient.getId() != null) {
             throw new IllegalArgumentException("Registry ID cannot be specified.");
         }
 
+        if (StringUtils.isBlank(requestRegistryClient.getName())) {
+            throw new IllegalArgumentException("Registry name must be specified.");
+        }
+
+        if (StringUtils.isBlank(requestRegistryClient.getUri())) {
+            throw new IllegalArgumentException("Registry URL must be specified.");
+        }
+
         if (isReplicateRequest()) {
             return replicate(HttpMethod.POST, requestRegistryClientEntity);
         }
@@ -468,7 +475,7 @@ public class ControllerResource extends ApplicationResource {
      *
      * @param httpServletRequest      request
      * @param id                      The id of the controller service to update.
-     * @param requestRegsitryEntity A controllerServiceEntity.
+     * @param requestRegistryEntity A controllerServiceEntity.
      * @return A controllerServiceEntity.
      */
     @PUT
@@ -501,32 +508,40 @@ public class ControllerResource extends ApplicationResource {
             @ApiParam(
                     value = "The registry configuration details.",
                     required = true
-            ) final RegistryClientEntity requestRegsitryEntity) {
+            ) final RegistryClientEntity requestRegistryEntity) {
 
-        if (requestRegsitryEntity == null || requestRegsitryEntity.getComponent() == null) {
+        if (requestRegistryEntity == null || requestRegistryEntity.getComponent() == null) {
             throw new IllegalArgumentException("Registry details must be specified.");
         }
 
-        if (requestRegsitryEntity.getRevision() == null) {
+        if (requestRegistryEntity.getRevision() == null) {
             throw new IllegalArgumentException("Revision must be specified.");
         }
 
         // ensure the ids are the same
-        final RegistryDTO requestRegistryDTO = requestRegsitryEntity.getComponent();
-        if (!id.equals(requestRegistryDTO.getId())) {
+        final RegistryDTO requestRegistryClient = requestRegistryEntity.getComponent();
+        if (!id.equals(requestRegistryClient.getId())) {
             throw new IllegalArgumentException(String.format("The registry id (%s) in the request body does not equal the "
-                    + "registry id of the requested resource (%s).", requestRegistryDTO.getId(), id));
+                    + "registry id of the requested resource (%s).", requestRegistryClient.getId(), id));
         }
 
         if (isReplicateRequest()) {
-            return replicate(HttpMethod.PUT, requestRegsitryEntity);
+            return replicate(HttpMethod.PUT, requestRegistryEntity);
+        }
+
+        if (requestRegistryClient.getName() != null && StringUtils.isBlank(requestRegistryClient.getName())) {
+            throw new IllegalArgumentException("Registry name must be specified.");
+        }
+
+        if (requestRegistryClient.getUri() != null && StringUtils.isBlank(requestRegistryClient.getUri())) {
+            throw new IllegalArgumentException("Registry URL must be specified.");
         }
 
         // handle expects request (usually from the cluster manager)
-        final Revision requestRevision = getRevision(requestRegsitryEntity, id);
+        final Revision requestRevision = getRevision(requestRegistryEntity, id);
         return withWriteLock(
                 serviceFacade,
-                requestRegsitryEntity,
+                requestRegistryEntity,
                 requestRevision,
                 lookup -> {
                     authorizeController(RequestAction.WRITE);

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 950bd97..3090c6e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -175,7 +175,10 @@ public class VersionsResource extends ApplicationResource {
             + "prevent any other threads from simultaneously saving local changes to Version Control. It will not, however, actually save the local flow to the Flow Registry. A "
             + "POST to /versions/process-groups/{id} should be used to initiate saving of the local flow to the Flow Registry.",
             response = String.class,
-            notes = NON_GUARANTEED_ENDPOINT)
+        notes = NON_GUARANTEED_ENDPOINT,
+        authorizations = {
+            @Authorization(value = "Write - /process-groups/{uuid}")
+        })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -186,14 +189,14 @@ public class VersionsResource extends ApplicationResource {
     public Response createVersionControlRequest(
             @ApiParam(value = "The versioned flow details.", required = true) final CreateActiveRequestEntity requestEntity) {
 
-        if (isReplicateRequest()) {
-            return replicate(HttpMethod.POST, requestEntity);
-        }
-
         if (requestEntity.getProcessGroupId() == null) {
             throw new IllegalArgumentException("The id of the process group that will be updated must be specified.");
         }
 
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestEntity);
+        }
+
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
         return withWriteLock(
@@ -234,7 +237,7 @@ public class VersionsResource extends ApplicationResource {
             response = VersionControlInformationEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
-                @Authorization(value = "Write - /process-groups/{uuid}")
+                @Authorization(value = "Only the user that submitted the request can update it")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -341,9 +344,12 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("active-requests/{id}")
     @ApiOperation(
-        value = "Deletes the Version Control Request with the given ID. This will allow other threads to save flows to the Flow Registry. See also the documentation "
-            + "for POSTing to /versions/active-requests for information regarding why this is done.",
-            notes = NON_GUARANTEED_ENDPOINT)
+            value = "Deletes the Version Control Request with the given ID. This will allow other threads to save flows to the Flow Registry. See also the documentation "
+                + "for POSTing to /versions/active-requests for information regarding why this is done.",
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Only the user that submitted the request can remove it")
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -801,6 +807,7 @@ public class VersionsResource extends ApplicationResource {
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
+                @Authorization(value = "Only the user that submitted the request can get it")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -824,6 +831,7 @@ public class VersionsResource extends ApplicationResource {
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
+                @Authorization(value = "Only the user that submitted the request can get it")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -881,6 +889,7 @@ public class VersionsResource extends ApplicationResource {
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
+                @Authorization(value = "Only the user that submitted the request can remove it")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -904,6 +913,7 @@ public class VersionsResource extends ApplicationResource {
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
+                @Authorization(value = "Only the user that submitted the request can remove it")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index 34f4997..6a73e3a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -224,6 +224,7 @@ public final class EntityFactory {
             entity.setStatus(status);
             entity.setId(dto.getId());
             entity.setPosition(dto.getPosition());
+
             entity.setInputPortCount(dto.getInputPortCount());
             entity.setOutputPortCount(dto.getOutputPortCount());
             entity.setRunningCount(dto.getRunningCount());
@@ -232,6 +233,17 @@ public final class EntityFactory {
             entity.setDisabledCount(dto.getDisabledCount());
             entity.setActiveRemotePortCount(dto.getActiveRemotePortCount());
             entity.setInactiveRemotePortCount(dto.getInactiveRemotePortCount());
+
+            entity.setUpToDateCount(dto.getUpToDateCount());
+            entity.setLocallyModifiedCount(dto.getLocallyModifiedCount());
+            entity.setStaleCount(dto.getStaleCount());
+            entity.setLocallyModifiedAndStaleCount(dto.getLocallyModifiedAndStaleCount());
+            entity.setSyncFailureCount(dto.getSyncFailureCount());
+
+            if (dto.getVersionControlInformation() != null) {
+                entity.setState(dto.getVersionControlInformation().getState());
+            }
+
             entity.setBulletins(bulletins); // include bulletins as authorized descendant component bulletins should be available
             if (permissions != null && permissions.getCanRead()) {
                 entity.setComponent(dto);
@@ -499,6 +511,11 @@ public final class EntityFactory {
         if (dto != null) {
             entity.setPermissions(permissions);
             entity.setId(dto.getId());
+
+            if (dto.getVersionControlInformation() != null) {
+                entity.setState(dto.getVersionControlInformation().getState());
+            }
+
             if (permissions != null && permissions.getCanRead()) {
                 entity.setBreadcrumb(dto);
             }

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
index 907c8dc..6cef841 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
@@ -593,6 +593,11 @@ public class ControllerFacade implements Authorizable {
         controllerStatus.setDisabledCount(counts.getDisabledCount());
         controllerStatus.setActiveRemotePortCount(counts.getActiveRemotePortCount());
         controllerStatus.setInactiveRemotePortCount(counts.getInactiveRemotePortCount());
+        controllerStatus.setUpToDateCount(counts.getUpToDateCount());
+        controllerStatus.setLocallyModifiedCount(counts.getLocallyModifiedCount());
+        controllerStatus.setStaleCount(counts.getStaleCount());
+        controllerStatus.setLocallyModifiedAndStaleCount(counts.getLocallyModifiedAndStaleCount());
+        controllerStatus.setSyncFailureCount(counts.getSyncFailureCount());
 
         return controllerStatus;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp
index 81e9ef0..9cb0ab9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp
@@ -26,6 +26,13 @@
         <div class="fa fa-stop"><span id="controller-stopped-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerStoppedCount}}</span></div>
         <div class="fa fa-warning"><span id="controller-invalid-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerInvalidCount}}</span></div>
         <div class="icon icon-enable-false"><span id="controller-disabled-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerDisabledCount}}</span></div>
+        <div class="fa fa-check"><span id="controller-up-to-date-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerUpToDateCount}}</span></div>
+        <div class="fa fa-asterisk"><span id="controller-locally-modified-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerLocallyModifiedCount}}</span></div>
+        <div class="fa fa-arrow-circle-up"><span id="controller-stale-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerStaleCount}}</span></div>
+        <div class="fa fa-exclamation-circle">
+            <span id="controller-locally-modified-and-stale-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerLocallyModifiedAndStaleCount}}</span>
+        </div>
+        <div class="fa fa-question"><span id="controller-sync-failure-count">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.controllerSyncFailureCount}}</span></div>
         <div class="fa fa-refresh"><span id="stats-last-refreshed">{{appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.statsLastRefreshed}}</span></div>
         <div id="canvas-loading-container" class="loading-container"></div>
     </div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
index 3f1b6a0..9066e9e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
@@ -24,7 +24,10 @@
             </div>
         </div>
         <div class="setting">
-            <span id="import-process-group-link" class="link"><i class="fa fa-cloud-download" aria-hidden="true" style="margin-left: 5px; margin-right: 5px;"></i>Import version...</span>
+            <span id="import-process-group-link" class="link" title="Import a flow from a registry">
+                <i class="fa fa-cloud-download" aria-hidden="true" style="margin-left: 5px; margin-right: 5px;"></i>
+                Import...
+            </span>
         </div>
     </div>
 </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
index ac4cb62..a7de12f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
@@ -417,7 +417,7 @@ button.fa {
     color: #004849;
     font-size: 16px;
     cursor: pointer;
-    line-height: 23px;
+    line-height: 25px;
 }
 
 button.icon {

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
index 85b499e..17200d7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
@@ -14,24 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* styles for the graph pan/zoom controls */
-
-/*
- * 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.
- */
 
 /* general graph control styles */
 
@@ -50,7 +32,7 @@
 
 #graph-controls .fa {
     font-size: 18px;
-    margin-left: -1px;
+    margin-left: -2px;
 }
 
 .graph-control-header-icon.fa {
@@ -93,7 +75,7 @@ div.graph-control-docked {
 }
 
 div.graph-control button {
-    line-height: 28px;
+    line-height: 30px;
     border: 1px solid #CCDADB; /*tint link-color 80%*/
     background-color: rgba(249,250,251,1);
     color: #004849;

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
index fc5599d..64f2117 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
@@ -103,7 +103,7 @@
              * @returns {*}
              */
             isTracking: function (breadcrumbEntity) {
-                return nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation);
+                return nfCommon.isDefinedAndNotNull(breadcrumbEntity.state);
             },
 
             /**
@@ -113,8 +113,8 @@
              * @returns {string}
              */
             getVersionControlClass: function (breadcrumbEntity) {
-                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
-                    var vciState = breadcrumbEntity.breadcrumb.versionControlInformation.state;
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.state)) {
+                    var vciState = breadcrumbEntity.state;
                     if (vciState === 'SYNC_FAILURE') {
                         return 'breadcrumb-version-control-gray fa fa-question'
                     } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
@@ -137,7 +137,7 @@
              * @param breadcrumbEntity
              */
             getVersionControlTooltip: function (breadcrumbEntity) {
-                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.state) && breadcrumbEntity.permissions.canRead) {
                     return nfCommon.getVersionControlTooltip(breadcrumbEntity.breadcrumb.versionControlInformation);
                 } else {
                     return 'This Process Group is not under version control.'

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
index 16f40f1..038a07b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
@@ -74,6 +74,11 @@
             this.controllerStoppedCount = "-";
             this.controllerInvalidCount = "-";
             this.controllerDisabledCount = "-";
+            this.controllerUpToDateCount = "-";
+            this.controllerLocallyModifiedCount = "-";
+            this.controllerStaleCount = "-";
+            this.controllerLocallyModifiedAndStaleCount = "-";
+            this.controllerSyncFailureCount = "-";
             this.statsLastRefreshed = "-";
 
             /**
@@ -502,6 +507,51 @@
                     $('#flow-status-container').find('.icon-enable-false').removeClass('disabled').addClass('zero');
                 }
 
+                this.controllerUpToDateCount =
+                    nfCommon.isDefinedAndNotNull(status.upToDateCount) ? status.upToDateCount : '-';
+
+                if (this.controllerUpToDateCount > 0) {
+                    $('#flow-status-container').find('.fa-check').removeClass('zero').addClass('up-to-date');
+                } else {
+                    $('#flow-status-container').find('.fa-check').removeClass('up-to-date').addClass('zero');
+                }
+
+                this.controllerLocallyModifiedCount =
+                    nfCommon.isDefinedAndNotNull(status.locallyModifiedCount) ? status.locallyModifiedCount : '-';
+
+                if (this.controllerLocallyModifiedCount > 0) {
+                    $('#flow-status-container').find('.fa-asterisk').removeClass('zero').addClass('locally-modified');
+                } else {
+                    $('#flow-status-container').find('.fa-asterisk').removeClass('locally-modified').addClass('zero');
+                }
+
+                this.controllerStaleCount =
+                    nfCommon.isDefinedAndNotNull(status.staleCount) ? status.staleCount : '-';
+
+                if (this.controllerStaleCount > 0) {
+                    $('#flow-status-container').find('.fa-arrow-circle-up').removeClass('zero').addClass('stale');
+                } else {
+                    $('#flow-status-container').find('.fa-arrow-circle-up').removeClass('stale').addClass('zero');
+                }
+
+                this.controllerLocallyModifiedAndStaleCount =
+                    nfCommon.isDefinedAndNotNull(status.locallyModifiedAndStaleCount) ? status.locallyModifiedAndStaleCount : '-';
+
+                if (this.controllerLocallyModifiedAndStaleCount > 0) {
+                    $('#flow-status-container').find('.fa-exclamation-circle').removeClass('zero').addClass('locally-modified-and-stale');
+                } else {
+                    $('#flow-status-container').find('.fa-exclamation-circle').removeClass('locally-modified-and-stale').addClass('zero');
+                }
+
+                this.controllerSyncFailureCount =
+                    nfCommon.isDefinedAndNotNull(status.syncFailureCount) ? status.syncFailureCount : '-';
+
+                if (this.controllerSyncFailureCount > 0) {
+                    $('#flow-status-container').find('.fa-question').removeClass('zero').addClass('sync-failure');
+                } else {
+                    $('#flow-status-container').find('.fa-question').removeClass('sync-failure').addClass('zero');
+                }
+
             },
 
             /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index f1b9a45..01a0a07 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -90,7 +90,7 @@
         $('#save-flow-version-bucket').text('').hide();
 
         $('#save-flow-version-name').text('').hide();
-        $('#save-flow-version-description').text('').hide();
+        $('#save-flow-version-description').removeClass('unset blank').text('').hide();
 
         $('#save-flow-version-name-field').val('').hide();
         $('#save-flow-version-description-field').val('').hide();
@@ -277,14 +277,17 @@
                     optionClass: 'unset',
                     disabled: true
                 });
-                flowCombo.combo('destroy').combo({
-                    options: [{
-                        text: 'No available flows',
-                        value: null,
-                        optionClass: 'unset',
-                        disabled: true
-                    }]
-                });
+
+                if (nfCommon.isDefinedAndNotNull(flowCombo)) {
+                    flowCombo.combo('destroy').combo({
+                        options: [{
+                            text: 'No available flows',
+                            value: null,
+                            optionClass: 'unset',
+                            disabled: true
+                        }]
+                    });
+                }
             }
 
             // load the buckets
@@ -315,14 +318,17 @@
                     disabled: true
                 }]
             });
-            flowCombo.combo('destroy').combo({
-                options: [{
-                    text: 'No available flows',
-                    value: null,
-                    optionClass: 'unset',
-                    disabled: true
-                }]
-            });
+
+            if (nfCommon.isDefinedAndNotNull(flowCombo)) {
+                flowCombo.combo('destroy').combo({
+                    options: [{
+                        text: 'No available flows',
+                        value: null,
+                        optionClass: 'unset',
+                        disabled: true
+                    }]
+                });
+            }
 
             dialog.modal('refreshButtons');
         };
@@ -390,8 +396,6 @@
                 registryId: versionControlInformation.registryId,
                 bucketId: versionControlInformation.bucketId,
                 flowId: versionControlInformation.flowId,
-                flowName: $('#save-flow-version-name').text(),
-                description: $('#save-flow-version-description').text(),
                 comments: $('#save-flow-version-change-comments').val()
             }
         } else {
@@ -1598,9 +1602,10 @@
                             var processGroupId = $('#save-flow-version-process-group-id').text();
                             saveFlowVersion().done(function (response) {
                                 updateVersionControlInformation(processGroupId, response.versionControlInformation);
-                            });
 
-                            $(this).modal('hide');
+                                // only hide the dialog if the flow version was successfully saved
+                                $(this).modal('hide');
+                            });
                         }
                     }
                 }, {
@@ -1729,9 +1734,6 @@
 
             return $.Deferred(function (deferred) {
                 getVersionControlInformation(processGroupId).done(function (groupVersionControlInformation) {
-                    // record the revision
-                    $('#save-flow-version-process-group-id').data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId);
-
                     if (nfCommon.isDefinedAndNotNull(groupVersionControlInformation.versionControlInformation)) {
                         var versionControlInformation = groupVersionControlInformation.versionControlInformation;
 
@@ -1741,7 +1743,8 @@
                         $('#save-flow-version-label').text(versionControlInformation.version + 1);
 
                         $('#save-flow-version-name').text(versionControlInformation.flowName).show();
-                        $('#save-flow-version-description').text(versionControlInformation.flowDescription).show();
+                        nfCommon.populateField('save-flow-version-description', versionControlInformation.flowDescription);
+                        $('#save-flow-version-description').show();
 
                         // record the versionControlInformation
                         $('#save-flow-version-process-group-id').data('versionControlInformation', versionControlInformation);
@@ -1787,6 +1790,9 @@
                             deferred.reject();
                         });
                     }
+
+                    // record the revision
+                    $('#save-flow-version-process-group-id').data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId);
                 }).fail(nfErrorHandler.handleAjaxError);
             }).done(function () {
                 $('#save-flow-version-dialog').modal('show');

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
index 66c5542..1799a86 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
@@ -368,7 +368,10 @@
                     port.select('text.port-name').text(null);
 
                     // clear the port comments
-                    port.select('path.component-comments').style('visibility', false);
+                    port.select('path.component-comments').style('visibility', 'hidden');
+
+                    // clear tooltips
+                    port.call(removeTooltips);
                 }
 
                 // populate the stats

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 27004b4..433c59f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -91,7 +91,7 @@
      * @param d
      */
     var isUnderVersionControl = function (d) {
-        return nfCommon.isDefinedAndNotNull(d.component.versionControlInformation);
+        return nfCommon.isDefinedAndNotNull(d.state);
     };
 
     /**
@@ -953,48 +953,152 @@
                         return d.disabledCount;
                     });
 
-                if (processGroupData.permissions.canRead) {
-                    // update version control information
-                    var versionControl = processGroup.select('text.version-control')
-                        .style({
-                            'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
-                            'fill': function () {
-                                if (isUnderVersionControl(processGroupData)) {
-                                    var vciState = processGroupData.component.versionControlInformation.state;
-                                    if (vciState === 'SYNC_FAILURE') {
-                                        return '#666666';
-                                    } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
-                                        return '#BA554A';
-                                    } else if (vciState === 'STALE') {
-                                        return '#BA554A';
-                                    } else if (vciState === 'LOCALLY_MODIFIED') {
-                                        return '#666666';
-                                    } else {
-                                        return '#1A9964';
-                                    }
-                                } else {
-                                    return '#000';
-                                }
-                            }
-                        })
-                        .text(function () {
+                // up to date current
+                var upToDate = details.select('text.process-group-up-to-date')
+                    .classed('up-to-date', function (d) {
+                        return d.permissions.canRead && d.component.upToDateCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.upToDateCount === 0;
+                    });
+                var upToDateCount = details.select('text.process-group-up-to-date-count')
+                    .attr('x', function () {
+                        var updateToDateCountX = parseInt(upToDate.attr('x'), 10);
+                        return updateToDateCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.upToDateCount;
+                    });
+
+                // update locally modified
+                var locallyModified = details.select('text.process-group-locally-modified')
+                    .classed('locally-modified', function (d) {
+                        return d.permissions.canRead && d.component.locallyModifiedCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.locallyModifiedCount === 0;
+                    })
+                    .attr('x', function () {
+                        var upToDateX = parseInt(upToDateCount.attr('x'), 10);
+                        return upToDateX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var locallyModifiedCount = details.select('text.process-group-locally-modified-count')
+                    .attr('x', function () {
+                        var locallyModifiedCountX = parseInt(locallyModified.attr('x'), 10);
+                        return locallyModifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.locallyModifiedCount;
+                    });
+
+                // update stale
+                var stale = details.select('text.process-group-stale')
+                    .classed('stale', function (d) {
+                        return d.permissions.canRead && d.component.staleCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.staleCount === 0;
+                    })
+                    .attr('x', function () {
+                        var locallyModifiedX = parseInt(locallyModifiedCount.attr('x'), 10);
+                        return locallyModifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var staleCount = details.select('text.process-group-stale-count')
+                    .attr('x', function () {
+                        var staleCountX = parseInt(stale.attr('x'), 10);
+                        return staleCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.staleCount;
+                    });
+
+                // update locally modified and stale
+                var locallyModifiedAndStale = details.select('text.process-group-locally-modified-and-stale')
+                    .classed('locally-modified-and-stale', function (d) {
+                        return d.permissions.canRead && d.component.locallyModifiedAndStaleCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.locallyModifiedAndStaleCount === 0;
+                    })
+                    .attr('x', function () {
+                        var staleX = parseInt(staleCount.attr('x'), 10);
+                        return staleX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var locallyModifiedAndStaleCount = details.select('text.process-group-locally-modified-and-stale-count')
+                    .attr('x', function () {
+                        var locallyModifiedAndStaleCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
+                        return locallyModifiedAndStaleCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.locallyModifiedAndStaleCount;
+                    });
+
+                // update sync failure
+                var syncFailure = details.select('text.process-group-sync-failure')
+                    .classed('sync-failure', function (d) {
+                        return d.permissions.canRead && d.component.syncFailureCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.syncFailureCount === 0;
+                    })
+                    .attr('x', function () {
+                        var syncFailureX = parseInt(locallyModifiedAndStaleCount.attr('x'), 10);
+                        return syncFailureX + Math.round(locallyModifiedAndStaleCount.node().getComputedTextLength()) + CONTENTS_SPACER - 2;
+                    });
+                details.select('text.process-group-sync-failure-count')
+                    .attr('x', function () {
+                        var syncFailureCountX = parseInt(syncFailure.attr('x'), 10);
+                        return syncFailureCountX + Math.round(syncFailure.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.syncFailureCount;
+                    });
+
+                // update version control information
+                var versionControl = processGroup.select('text.version-control')
+                    .style({
+                        'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
+                        'fill': function () {
                             if (isUnderVersionControl(processGroupData)) {
-                                var vciState = processGroupData.component.versionControlInformation.state;
+                                var vciState = processGroupData.state;
                                 if (vciState === 'SYNC_FAILURE') {
-                                    return '\uf128'
+                                    return '#666666';
                                 } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
-                                    return '\uf06a';
+                                    return '#BA554A';
                                 } else if (vciState === 'STALE') {
-                                    return '\uf0aa';
+                                    return '#BA554A';
                                 } else if (vciState === 'LOCALLY_MODIFIED') {
-                                    return '\uf069';
+                                    return '#666666';
                                 } else {
-                                    return '\uf00c';
+                                    return '#1A9964';
                                 }
                             } else {
-                                return '';
+                                return '#000';
                             }
-                        }).each(function () {
+                        }
+                    })
+                    .text(function () {
+                        if (isUnderVersionControl(processGroupData)) {
+                            var vciState = processGroupData.state;
+                            if (vciState === 'SYNC_FAILURE') {
+                                return '\uf128'
+                            } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
+                                return '\uf06a';
+                            } else if (vciState === 'STALE') {
+                                return '\uf0aa';
+                            } else if (vciState === 'LOCALLY_MODIFIED') {
+                                return '\uf069';
+                            } else {
+                                return '\uf00c';
+                            }
+                        } else {
+                            return '';
+                        }
+                    });
+
+                if (processGroupData.permissions.canRead) {
+                    // version control tooltip
+                    versionControl.each(function () {
                             // get the tip
                             var tip = d3.select('#version-control-tip-' + processGroupData.id);
 
@@ -1092,136 +1196,10 @@
                         .text(function (d) {
                             return d.component.name;
                         });
-
-                    // up to date current
-                    var upToDate = details.select('text.process-group-up-to-date')
-                        .style('visibility', 'visible')
-                        .classed('up-to-date', function (d) {
-                            return d.component.upToDateCount > 0;
-                        })
-                        .classed('zero', function (d) {
-                            return d.component.upToDateCount === 0;
-                        });
-                    var upToDateCount = details.select('text.process-group-up-to-date-count')
-                        .style('visibility', 'visible')
-                        .attr('x', function () {
-                            var updateToDateCountX = parseInt(upToDate.attr('x'), 10);
-                            return updateToDateCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
-                        })
-                        .text(function (d) {
-                            return d.component.upToDateCount;
-                        });
-
-                    // update locally modified
-                    var locallyModified = details.select('text.process-group-locally-modified')
-                        .style('visibility', 'visible')
-                        .classed('locally-modified', function (d) {
-                            return d.component.locallyModifiedCount > 0;
-                        })
-                        .classed('zero', function (d) {
-                            return d.component.locallyModifiedCount === 0;
-                        })
-                        .attr('x', function () {
-                            var upToDateX = parseInt(upToDateCount.attr('x'), 10);
-                            return upToDateX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
-                        });
-                    var locallyModifiedCount = details.select('text.process-group-locally-modified-count')
-                        .style('visibility', 'visible')
-                        .attr('x', function () {
-                            var locallyModifiedCountX = parseInt(locallyModified.attr('x'), 10);
-                            return locallyModifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
-                        })
-                        .text(function (d) {
-                            return d.component.locallyModifiedCount;
-                        });
-
-                    // update stale
-                    var stale = details.select('text.process-group-stale')
-                        .style('visibility', 'visible')
-                        .classed('stale', function (d) {
-                            return d.component.staleCount > 0;
-                        })
-                        .classed('zero', function (d) {
-                            return d.component.staleCount === 0;
-                        })
-                        .attr('x', function () {
-                            var locallyModifiedX = parseInt(locallyModifiedCount.attr('x'), 10);
-                            return locallyModifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
-                        });
-                    var staleCount = details.select('text.process-group-stale-count')
-                        .style('visibility', 'visible')
-                        .attr('x', function () {
-                            var staleCountX = parseInt(stale.attr('x'), 10);
-                            return staleCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
-                        })
-                        .text(function (d) {
-                            return d.component.staleCount;
-                        });
-
-                    // update locally modified and stale
-                    var locallyModifiedAndStale = details.select('text.process-group-locally-modified-and-stale')
-                        .style('visibility', 'visible')
-                        .classed('locally-modified-and-stale', function (d) {
-                            return d.component.locallyModifiedAndStaleCount > 0;
-                        })
-                        .classed('zero', function (d) {
-                            return d.component.locallyModifiedAndStaleCount === 0;
-                        })
-                        .attr('x', function () {
-                            var staleX = parseInt(staleCount.attr('x'), 10);
-                            return staleX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
-                        });
-                    var locallyModifiedAndStaleCount = details.select('text.process-group-locally-modified-and-stale-count')
-                        .style('visibility', 'visible')
-                        .attr('x', function () {
-                            var locallyModifiedAndStaleCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
-                            return locallyModifiedAndStaleCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
-                        })
-                        .text(function (d) {
-                            return d.component.locallyModifiedAndStaleCount;
-                        });
-
-                    // update sync failure
-                    var syncFailure = details.select('text.process-group-sync-failure')
-                        .style('visibility', 'visible')
-                        .classed('sync-failure', function (d) {
-                            return d.component.syncFailureCount > 0;
-                        })
-                        .classed('zero', function (d) {
-                            return d.component.syncFailureCount === 0;
-                        })
-                        .attr('x', function () {
-                            var syncFailureX = parseInt(locallyModifiedAndStaleCount.attr('x'), 10);
-                            return syncFailureX + Math.round(locallyModifiedAndStaleCount.node().getComputedTextLength()) + CONTENTS_SPACER - 2;
-                        });
-                    details.select('text.process-group-sync-failure-count')
-                        .style('visibility', 'visible')
-                        .attr('x', function () {
-                            var syncFailureCountX = parseInt(syncFailure.attr('x'), 10);
-                            return syncFailureCountX + Math.round(syncFailure.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
-                        })
-                        .text(function (d) {
-                            return d.component.syncFailureCount;
-                        });
                 } else {
-                    // update version control information
-                    processGroup.select('text.version-control').style('visibility', 'hidden');
-
                     // clear the process group comments
                     processGroup.select('path.component-comments').style('visibility', 'hidden');
 
-                    // clear the encapsulate versioned pg counts
-                    details.select('text.process-group-up-to-date').style('visibility', 'hidden');
-                    details.select('text.process-group-up-to-date-count').style('visibility', 'hidden');
-                    details.select('text.process-group-locally-modified').style('visibility', 'hidden');
-                    details.select('text.process-group-locally-modified-count').style('visibility', 'hidden');
-                    details.select('text.process-group-stale').style('visibility', 'hidden');
-                    details.select('text.process-group-stale-count').style('visibility', 'hidden');
-                    details.select('text.process-group-locally-modified-and-stale').style('visibility', 'hidden');
-                    details.select('text.process-group-locally-modified-and-stale-count').style('visibility', 'hidden');
-                    details.select('text.process-group-sync-failure').style('visibility', 'hidden');
-                    details.select('text.process-group-sync-failure-count').style('visibility', 'hidden');
-
                     // clear the process group name
                     processGroup.select('text.process-group-name')
                         .attr({
@@ -1229,6 +1207,9 @@
                             'width': 316
                         })
                         .text(null);
+
+                    // clear tooltips
+                    processGroup.call(removeTooltips);
                 }
 
                 // populate the stats

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
index 77b9af8..8b991a3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
@@ -661,7 +661,10 @@
                     processor.select('text.processor-bundle').text(null);
 
                     // clear the processor comments
-                    processor.select('path.component-comments').style('visibility', false);
+                    processor.select('path.component-comments').style('visibility', 'hidden');
+
+                    // clear tooltips
+                    processor.call(removeTooltips);
                 }
 
                 // populate the stats

http://git-wip-us.apache.org/repos/asf/nifi/blob/1266235c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
index 5de0164..a58d8db 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
@@ -288,18 +288,6 @@
                             'fill': '#ffffff'
                         });
 
-                    // border
-                    details.append('rect')
-                        .attr({
-                            'width': function () {
-                                return remoteProcessGroupData.dimensions.width;
-                            },
-                            'height': 1,
-                            'x': 0,
-                            'y': 103,
-                            'fill': '#c7d2d7'
-                        });
-
                     // -----
                     // stats
                     // -----
@@ -499,7 +487,7 @@
                             'x': function () {
                                 return remoteProcessGroupData.dimensions.width - 17;
                             },
-                            'y': 50
+                            'y': 49
                         })
                         .text('\uf24a');
                 }
@@ -625,13 +613,16 @@
                     details.select('text.remote-process-group-transmission-secure').text(null);
 
                     // clear the comments
-                    details.select('path.component-comments').style('visibility', false);
+                    details.select('path.component-comments').style('visibility', 'hidden');
 
                     // clear the last refresh
                     details.select('text.remote-process-group-last-refresh').text(null);
 
                     // clear the name
                     remoteProcessGroup.select('text.remote-process-group-name').text(null);
+
+                    // clear tooltips
+                    remoteProcessGroup.call(removeTooltips);
                 }
 
                 // populate the stats


[25/50] nifi git commit: NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated autho

Posted by bb...@apache.org.
NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated authorizations required for version control endpoints - Add state and percent complete fields ot VersionedFlowUpdateRequestDTO

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/6b00dff1
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/6b00dff1
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/6b00dff1

Branch: refs/heads/master
Commit: 6b00dff1a88a00d87dc77480853cc7c5637ab774
Parents: 696d583
Author: Mark Payne <ma...@hotmail.com>
Authored: Sat Nov 4 14:19:49 2017 -0400
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:53 2018 -0500

----------------------------------------------------------------------
 .../web/api/dto/ComponentDifferenceDTO.java     | 100 ++++++
 .../api/dto/VersionControlInformationDTO.java   |  30 ++
 .../api/dto/VersionedFlowUpdateRequestDTO.java  |  30 ++
 .../web/api/entity/FlowComparisonEntity.java    |  40 +++
 .../nifi-framework-core-api/pom.xml             |  31 +-
 .../apache/nifi/registry/flow/FlowRegistry.java |  66 +++-
 .../registry/flow/UnknownResourceException.java |  33 --
 .../flow/VersionControlInformation.java         |  20 ++
 .../nifi-framework/nifi-framework-core/pom.xml  |   4 +
 .../nifi/connectable/StandardConnection.java    |   6 +-
 .../controller/StandardFlowSynchronizer.java    |  19 +-
 .../serialization/FlowFromDOMFactory.java       |   3 +
 .../serialization/StandardFlowSerializer.java   |   3 +
 .../nifi/groups/StandardProcessGroup.java       | 316 ++++++++++++-------
 .../registry/flow/FileBasedFlowRegistry.java    |  65 +++-
 .../registry/flow/RestBasedFlowRegistry.java    | 235 ++++++++++++++
 .../flow/StandardFlowRegistryClient.java        |  20 ++
 .../flow/StandardVersionControlInformation.java | 149 ++++++++-
 .../flow/mapping/NiFiRegistryFlowMapper.java    |  30 +-
 .../src/main/resources/FlowConfiguration.xsd    |   5 +-
 .../src/main/resources/nifi-context.xml         |   4 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  45 ++-
 .../nifi/web/StandardNiFiServiceFacade.java     |  98 +++++-
 .../org/apache/nifi/web/api/FlowResource.java   |   3 +-
 .../nifi/web/api/ProcessGroupResource.java      |  61 +++-
 .../apache/nifi/web/api/VersionsResource.java   | 242 +++++++-------
 .../web/api/concurrent/AsyncRequestManager.java |   1 -
 .../api/concurrent/AsynchronousWebRequest.java  |  28 +-
 .../StandardAsynchronousWebRequest.java         |  52 ++-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  64 +++-
 .../nifi/web/dao/impl/FlowRegistryDAO.java      |  51 ++-
 .../nifi/web/dao/impl/StandardInputPortDAO.java |   8 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |  24 +-
 .../nifi/web/util/CancellableTimedPause.java    |   9 +-
 pom.xml                                         |   6 +
 35 files changed, 1494 insertions(+), 407 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
new file mode 100644
index 0000000..8febce0
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
@@ -0,0 +1,100 @@
+/*
+ * 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.web.api.dto;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.xml.bind.annotation.XmlType;
+
+import io.swagger.annotations.ApiModelProperty;
+
+@XmlType(name = "componentDifference")
+public class ComponentDifferenceDTO {
+    private String componentType;
+    private String componentId;
+    private String componentName;
+    private String processGroupId;
+    private List<String> differences;
+
+    @ApiModelProperty("The type of component")
+    public String getComponentType() {
+        return componentType;
+    }
+
+    public void setComponentType(String componentType) {
+        this.componentType = componentType;
+    }
+
+    @ApiModelProperty("The ID of the component")
+    public String getComponentId() {
+        return componentId;
+    }
+
+    public void setComponentId(String componentId) {
+        this.componentId = componentId;
+    }
+
+    @ApiModelProperty("The name of the component")
+    public String getComponentName() {
+        return componentName;
+    }
+
+    public void setComponentName(String componentName) {
+        this.componentName = componentName;
+    }
+
+    @ApiModelProperty("The ID of the Process Group that the component belongs to")
+    public String getProcessGroupId() {
+        return processGroupId;
+    }
+
+    public void setProcessGroupId(String processGroupId) {
+        this.processGroupId = processGroupId;
+    }
+
+    @ApiModelProperty("The differences in the component between the two flows")
+    public List<String> getDifferences() {
+        return differences;
+    }
+
+    public void setDifferences(List<String> differences) {
+        this.differences = differences;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(componentId);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof ComponentDifferenceDTO)) {
+            return false;
+        }
+
+        final ComponentDifferenceDTO other = (ComponentDifferenceDTO) obj;
+        return componentId.equals(other.getComponentId());
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
index e9aa246..c31a957 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
@@ -25,9 +25,12 @@ import javax.xml.bind.annotation.XmlType;
 public class VersionControlInformationDTO {
     private String groupId;
     private String registryId;
+    private String registryName;
     private String bucketId;
+    private String bucketName;
     private String flowId;
     private String flowName;
+    private String flowDescription;
     private Integer version;
     private Boolean modified;
     private Boolean current;
@@ -50,6 +53,15 @@ public class VersionControlInformationDTO {
         this.registryId = registryId;
     }
 
+    @ApiModelProperty(value = "The name of the registry that the flow is stored in", readOnly = true)
+    public String getRegistryName() {
+        return registryName;
+    }
+
+    public void setRegistryName(final String registryName) {
+        this.registryName = registryName;
+    }
+
     @ApiModelProperty("The ID of the bucket that the flow is stored in")
     public String getBucketId() {
         return bucketId;
@@ -59,6 +71,15 @@ public class VersionControlInformationDTO {
         this.bucketId = bucketId;
     }
 
+    @ApiModelProperty(value = "The name of the bucket that the flow is stored in", readOnly = true)
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public void setBucketName(String bucketName) {
+        this.bucketName = bucketName;
+    }
+
     @ApiModelProperty("The ID of the flow")
     public String getFlowId() {
         return flowId;
@@ -77,6 +98,15 @@ public class VersionControlInformationDTO {
         this.flowName = flowName;
     }
 
+    @ApiModelProperty("The description of the flow")
+    public String getFlowDescription() {
+        return flowDescription;
+    }
+
+    public void setFlowDescription(String flowDescription) {
+        this.flowDescription = flowDescription;
+    }
+
     @ApiModelProperty("The version of the flow")
     public Integer getVersion() {
         return version;

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
index aa42bf6..013b40d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
@@ -32,6 +32,9 @@ public class VersionedFlowUpdateRequestDTO {
     private Date lastUpdated;
     private boolean complete = false;
     private String failureReason;
+    private int percentComplete;
+    private String state;
+    private VersionControlInformationDTO versionControlInformation;
 
     @ApiModelProperty("The unique ID of the Process Group that the variable registry belongs to")
     public String getProcessGroupId() {
@@ -87,4 +90,31 @@ public class VersionedFlowUpdateRequestDTO {
     public void setFailureReason(String reason) {
         this.failureReason = reason;
     }
+
+    @ApiModelProperty(value = "The VersionControlInformation that describes where the Versioned Flow is located; this may not be populated until the request is completed.", readOnly = true)
+    public VersionControlInformationDTO getVersionControlInformation() {
+        return versionControlInformation;
+    }
+
+    public void setVersionControlInformation(VersionControlInformationDTO versionControlInformation) {
+        this.versionControlInformation = versionControlInformation;
+    }
+
+    @ApiModelProperty(value = "The state of the request", readOnly = true)
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    @ApiModelProperty(value = "The percentage complete for the request, between 0 and 100", readOnly = true)
+    public int getPercentComplete() {
+        return percentComplete;
+    }
+
+    public void setPercentComplete(int percentComplete) {
+        this.percentComplete = percentComplete;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowComparisonEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowComparisonEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowComparisonEntity.java
new file mode 100644
index 0000000..bc0251b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowComparisonEntity.java
@@ -0,0 +1,40 @@
+/*
+ * 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.web.api.entity;
+
+import java.util.Set;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.apache.nifi.web.api.dto.ComponentDifferenceDTO;
+
+import io.swagger.annotations.ApiModelProperty;
+
+@XmlRootElement(name = "flowComparisonEntity")
+public class FlowComparisonEntity extends Entity {
+    private Set<ComponentDifferenceDTO> componentDifferences;
+
+    @ApiModelProperty("The list of differences for each component in the flow that is not the same between the two flows")
+    public Set<ComponentDifferenceDTO> getComponentDifferences() {
+        return componentDifferences;
+    }
+
+    public void setComponentDifferences(Set<ComponentDifferenceDTO> componentDifferences) {
+        this.componentDifferences = componentDifferences;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
index d1bce36..3b86428 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
@@ -1,19 +1,16 @@
 <?xml version="1.0"?>
-<!--
-  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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.apache.nifi</groupId>
@@ -62,5 +59,9 @@
             <groupId>org.apache.nifi.registry</groupId>
             <artifactId>nifi-registry-data-model</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-client</artifactId>
+        </dependency>
     </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
index 4efff94..10db9cf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -19,6 +19,7 @@ package org.apache.nifi.registry.flow;
 
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 
 import java.io.IOException;
 import java.util.Set;
@@ -71,7 +72,47 @@ public interface FlowRegistry {
      * @param user current user
      * @return buckets for this user
      */
-    Set<Bucket> getBuckets(NiFiUser user) throws IOException;
+    Set<Bucket> getBuckets(NiFiUser user) throws IOException, NiFiRegistryException;
+
+    /**
+     * Gets the bucket with the given ID
+     *
+     * @param bucketId the id of the bucket
+     * @param user user on whose behalf the request is being made
+     * @return the bucket with the given ID
+     */
+    Bucket getBucket(String bucketId, NiFiUser user) throws IOException, NiFiRegistryException;
+
+    /**
+     * Gets the bucket with the given ID
+     *
+     * @param bucketId the id of the bucket
+     * @return the bucket with the given ID
+     */
+    Bucket getBucket(String bucketId) throws IOException, NiFiRegistryException;
+
+    /**
+     * Retrieves the set of all Versioned Flows for the specified bucket
+     *
+     * @param bucketId the ID of the bucket
+     * @param user the user on whose behalf the request is being made
+     * @return the set of all Versioned Flows for the specified bucket
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws NiFiRegistryException if unable to find the bucket with the given ID or the flow with the given ID
+     */
+    Set<VersionedFlow> getFlows(String bucketId, NiFiUser user) throws IOException, NiFiRegistryException;
+
+    /**
+     * Retrieves the set of all versions of the specified flow
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @param user the user on whose behalf the request is being made
+     * @return the set of all versions of the specified flow
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws NiFiRegistryException if unable to find the bucket with the given ID or the flow with the given ID
+     */
+    Set<VersionedFlowSnapshotMetadata> getFlowVersions(String bucketId, String flowId, NiFiUser user) throws IOException, NiFiRegistryException;
 
     /**
      * Registers the given Versioned Flow with the Flow Registry
@@ -80,9 +121,9 @@ public interface FlowRegistry {
      * @return the fully populated VersionedFlow
      *
      * @throws NullPointerException if the VersionedFlow is null, or if its bucket identifier or name is null
-     * @throws UnknownResourceException if the bucket id does not exist
+     * @throws NiFiRegistryException if the bucket id does not exist
      */
-    VersionedFlow registerVersionedFlow(VersionedFlow flow) throws IOException, UnknownResourceException;
+    VersionedFlow registerVersionedFlow(VersionedFlow flow) throws IOException, NiFiRegistryException;
 
     /**
      * Adds the given snapshot to the Flow Registry for the given flow
@@ -90,13 +131,14 @@ public interface FlowRegistry {
      * @param flow the Versioned Flow
      * @param snapshot the snapshot of the flow
      * @param comments any comments for the snapshot
+     * @param expectedVersion the version of the flow that we expect to save this snapshot as
      * @return the versioned flow snapshot
      *
      * @throws IOException if unable to communicate with the registry
      * @throws NullPointerException if the VersionedFlow is null, or if its bucket identifier is null, or if the flow to snapshot is null
-     * @throws UnknownResourceException if the flow does not exist
+     * @throws NiFiRegistryException if the flow does not exist
      */
-    VersionedFlowSnapshot registerVersionedFlowSnapshot(VersionedFlow flow, VersionedProcessGroup snapshot, String comments) throws IOException, UnknownResourceException;
+    VersionedFlowSnapshot registerVersionedFlowSnapshot(VersionedFlow flow, VersionedProcessGroup snapshot, String comments, int expectedVersion) throws IOException, NiFiRegistryException;
 
     /**
      * Returns the latest (most recent) version of the Flow in the Flow Registry for the given bucket and flow
@@ -106,9 +148,9 @@ public interface FlowRegistry {
      * @return the latest version of the Flow
      *
      * @throws IOException if unable to communicate with the Flow Registry
-     * @throws UnknownResourceException if unable to find the bucket with the given ID or the flow with the given ID
+     * @throws NiFiRegistryException if unable to find the bucket with the given ID or the flow with the given ID
      */
-    int getLatestVersion(String bucketId, String flowId) throws IOException, UnknownResourceException;
+    int getLatestVersion(String bucketId, String flowId) throws IOException, NiFiRegistryException;
 
     /**
      * Retrieves the contents of the Flow with the given Bucket ID, Flow ID, and version, from the Flow Registry
@@ -119,12 +161,13 @@ public interface FlowRegistry {
      * @return the contents of the Flow from the Flow Registry
      *
      * @throws IOException if unable to communicate with the Flow Registry
-     * @throws UnknownResourceException if unable to find the contents of the flow due to the bucket or flow not existing,
+     * @throws NiFiRegistryException if unable to find the contents of the flow due to the bucket or flow not existing,
      *             or the specified version of the flow not existing
      * @throws NullPointerException if any of the arguments is not specified
      * @throws IllegalArgumentException if the given version is less than 1
      */
-    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version) throws IOException, UnknownResourceException;
+    // TODO: Need to pass in user
+    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version) throws IOException, NiFiRegistryException;
 
     /**
      * Retrieves a VersionedFlow by bucket id and flow id
@@ -134,7 +177,8 @@ public interface FlowRegistry {
      * @return the VersionedFlow for the given bucket and flow ID's
      *
      * @throws IOException if unable to communicate with the Flow Registry
-     * @throws UnknownResourceException if unable to find a flow with the given bucket ID and flow ID
+     * @throws NiFiRegistryException if unable to find a flow with the given bucket ID and flow ID
      */
-    VersionedFlow getVersionedFlow(String bucketId, String flowId) throws IOException, UnknownResourceException;
+    // TODO: Need to pass in user
+    VersionedFlow getVersionedFlow(String bucketId, String flowId) throws IOException, NiFiRegistryException;
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java
deleted file mode 100644
index 8c95e67..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.registry.flow;
-
-public class UnknownResourceException extends Exception {
-
-    public UnknownResourceException(String message) {
-        super(message);
-    }
-
-    public UnknownResourceException(String message, Throwable cause) {
-        super(message, cause);
-    }
-
-    public UnknownResourceException(Throwable cause) {
-        super(cause);
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
index ea70b1c..67c3635 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
@@ -33,16 +33,36 @@ public interface VersionControlInformation {
     String getRegistryIdentifier();
 
     /**
+     * @return the name of the Flow Registry that this flow is tracking to
+     */
+    String getRegistryName();
+
+    /**
      * @return the unique identifier of the bucket that this flow belongs to
      */
     String getBucketIdentifier();
 
     /**
+     * @return the name of the bucket that this flow belongs to
+     */
+    String getBucketName();
+
+    /**
      * @return the unique identifier of this flow in the Flow Registry
      */
     String getFlowIdentifier();
 
     /**
+     * @return the name of the flow
+     */
+    String getFlowName();
+
+    /**
+     * @return the description of the flow
+     */
+    String getFlowDescription();
+
+    /**
      * @return the version of the flow in the Flow Registry that this flow is based on.
      */
     int getVersion();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
index 6548004..8edf394 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
@@ -161,6 +161,10 @@
             <artifactId>nifi-registry-flow-diff</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-client</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-core</artifactId>
             <version>${jackson.version}</version>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
index 7aa3003..d6ea4b5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
@@ -270,8 +270,10 @@ public final class StandardConnection implements Connection {
             return;
         }
 
-        if (getSource().isRunning()) {
-            throw new IllegalStateException("Cannot update the relationships for Connection because the source of the Connection is running");
+        try {
+            getSource().verifyCanUpdate();
+        } catch (final IllegalStateException ise) {
+            throw new IllegalStateException("Cannot update the relationships for Connection", ise);
         }
 
         try {

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index 5a7aeec..71a587c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -85,9 +85,9 @@ import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.SimpleProcessLogger;
+import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
-import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
@@ -1110,14 +1110,15 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
 
         final VersionControlInformationDTO versionControlInfoDto = processGroupDTO.getVersionControlInformation();
         if (versionControlInfoDto != null) {
-            final String registryId = versionControlInfoDto.getRegistryId();
-            final String bucketId = versionControlInfoDto.getBucketId();
-            final String flowId = versionControlInfoDto.getFlowId();
-            final int version = versionControlInfoDto.getVersion();
-            final boolean modified = false;
-            final boolean current = true;
-
-            final VersionControlInformation versionControlInformation = new StandardVersionControlInformation(registryId, bucketId, flowId, version, null, modified, current);
+            final FlowRegistry flowRegistry = controller.getFlowRegistryClient().getFlowRegistry(versionControlInfoDto.getRegistryId());
+            final String registryName = flowRegistry == null ? versionControlInfoDto.getRegistryId() : flowRegistry.getName();
+
+            final StandardVersionControlInformation versionControlInformation = StandardVersionControlInformation.Builder.fromDto(versionControlInfoDto)
+                .registryName(registryName)
+                .modified(false)
+                .current(true)
+                .build();
+
             // pass empty map for the version control mapping because the VersionedComponentId has already been set on the components
             processGroup.setVersionControlInformation(versionControlInformation, Collections.emptyMap());
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
index a2a589a..e95845a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
@@ -230,7 +230,10 @@ public class FlowFromDOMFactory {
         final VersionControlInformationDTO dto = new VersionControlInformationDTO();
         dto.setRegistryId(getString(versionControlInfoElement, "registryId"));
         dto.setBucketId(getString(versionControlInfoElement, "bucketId"));
+        dto.setBucketName(getString(versionControlInfoElement, "bucketName"));
         dto.setFlowId(getString(versionControlInfoElement, "flowId"));
+        dto.setFlowName(getString(versionControlInfoElement, "flowName"));
+        dto.setFlowDescription(getString(versionControlInfoElement, "flowDescription"));
         dto.setVersion(getInt(versionControlInfoElement, "version"));
         return dto;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
index f921bc6..b0fe508 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
@@ -190,7 +190,10 @@ public class StandardFlowSerializer implements FlowSerializer {
             final Element versionControlInfoElement = doc.createElement("versionControlInformation");
             addTextElement(versionControlInfoElement, "registryId", versionControlInfo.getRegistryIdentifier());
             addTextElement(versionControlInfoElement, "bucketId", versionControlInfo.getBucketIdentifier());
+            addTextElement(versionControlInfoElement, "bucketName", versionControlInfo.getBucketName());
             addTextElement(versionControlInfoElement, "flowId", versionControlInfo.getFlowIdentifier());
+            addTextElement(versionControlInfoElement, "flowName", versionControlInfo.getFlowName());
+            addTextElement(versionControlInfoElement, "flowDescription", versionControlInfo.getFlowDescription());
             addTextElement(versionControlInfoElement, "version", versionControlInfo.getVersion());
             element.appendChild(versionControlInfoElement);
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 1d8652e..2783e96 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -16,6 +16,30 @@
  */
 package org.apache.nifi.groups;
 
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -68,16 +92,17 @@ import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.StandardProcessContext;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableDescriptor;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.Bundle;
 import org.apache.nifi.registry.flow.ConnectableComponent;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
-import org.apache.nifi.registry.flow.UnknownResourceException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedControllerService;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFunnel;
 import org.apache.nifi.registry.flow.VersionedLabel;
@@ -88,11 +113,13 @@ import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
 import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.DifferenceType;
+import org.apache.nifi.registry.flow.diff.EvolvingDifferenceDescriptor;
 import org.apache.nifi.registry.flow.diff.FlowComparator;
 import org.apache.nifi.registry.flow.diff.FlowComparison;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
+import org.apache.nifi.registry.flow.diff.StaticDifferenceDescriptor;
 import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -109,30 +136,6 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import static java.util.Objects.requireNonNull;
-
 public final class StandardProcessGroup implements ProcessGroup {
 
     private final String id;
@@ -2812,7 +2815,9 @@ public final class StandardProcessGroup implements ProcessGroup {
 
     @Override
     public void setVersionControlInformation(final VersionControlInformation versionControlInformation, final Map<String, String> versionedComponentIds) {
-        final StandardVersionControlInformation svci = new StandardVersionControlInformation(versionControlInformation.getRegistryIdentifier(),
+        final StandardVersionControlInformation svci = new StandardVersionControlInformation(
+            versionControlInformation.getRegistryIdentifier(),
+            versionControlInformation.getRegistryName(),
             versionControlInformation.getBucketIdentifier(),
             versionControlInformation.getFlowIdentifier(),
             versionControlInformation.getVersion(),
@@ -2822,10 +2827,19 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             @Override
             public Optional<Boolean> getModified() {
-                return StandardProcessGroup.this.isModified();
+                final Set<FlowDifference> differences = StandardProcessGroup.this.getModifications();
+                if (differences == null) {
+                    return Optional.ofNullable(null);
+                }
+
+                return Optional.of(!differences.isEmpty());
             }
         };
 
+        svci.setBucketName(versionControlInformation.getBucketName());
+        svci.setFlowName(versionControlInformation.getFlowName());
+        svci.setFlowDescription(versionControlInformation.getFlowDescription());
+
         writeLock.lock();
         try {
             updateVersionedComponentIds(this, versionedComponentIds);
@@ -2906,21 +2920,6 @@ public final class StandardProcessGroup implements ProcessGroup {
             return;
         }
 
-        try {
-            final int latestVersion = flowRegistry.getLatestVersion(vci.getBucketIdentifier(), vci.getFlowIdentifier());
-
-            if (latestVersion == vci.getVersion()) {
-                LOG.debug("{} is currently at the most recent version ({}) of the flow that is under Version Control", this, latestVersion);
-                vci.setCurrent(true);
-            } else {
-                vci.setCurrent(false);
-                LOG.info("{} is not the most recent version of the flow that is under Version Control; current version is {}; most recent version is {}",
-                    new Object[] {this, vci.getVersion(), latestVersion});
-            }
-        } catch (final IOException | UnknownResourceException e) {
-            LOG.error("Failed to synchronize {} with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry", this, e);
-        }
-
         final VersionedProcessGroup snapshot = vci.getFlowSnapshot();
         if (snapshot == null) {
             // We have not yet obtained the snapshot from the Flow Registry, so we need to request the snapshot of our local version of the flow from the Flow Registry.
@@ -2929,12 +2928,33 @@ public final class StandardProcessGroup implements ProcessGroup {
                 final VersionedFlowSnapshot registrySnapshot = flowRegistry.getFlowContents(vci.getBucketIdentifier(), vci.getFlowIdentifier(), vci.getVersion());
                 final VersionedProcessGroup registryFlow = registrySnapshot.getFlowContents();
                 vci.setFlowSnapshot(registryFlow);
-            } catch (final IOException | UnknownResourceException e) {
+            } catch (final IOException | NiFiRegistryException e) {
                 LOG.error("Failed to synchronize {} with Flow Registry because could not retrieve version {} of flow with identifier {} in bucket {}",
                     new Object[] {this, vci.getVersion(), vci.getFlowIdentifier(), vci.getBucketIdentifier()}, e);
                 return;
             }
         }
+
+        try {
+            final VersionedFlow versionedFlow = flowRegistry.getVersionedFlow(vci.getBucketIdentifier(), vci.getFlowIdentifier());
+            final int latestVersion = (int) versionedFlow.getVersionCount();
+
+            vci.setBucketName(versionedFlow.getBucketName());
+            vci.setFlowName(versionedFlow.getName());
+            vci.setFlowDescription(versionedFlow.getDescription());
+            vci.setRegistryName(flowRegistry.getName());
+
+            if (latestVersion == vci.getVersion()) {
+                LOG.debug("{} is currently at the most recent version ({}) of the flow that is under Version Control", this, latestVersion);
+                vci.setCurrent(true);
+            } else {
+                vci.setCurrent(false);
+                LOG.info("{} is not the most recent version of the flow that is under Version Control; current version is {}; most recent version is {}",
+                    new Object[] {this, vci.getVersion(), latestVersion});
+            }
+        } catch (final IOException | NiFiRegistryException e) {
+            LOG.error("Failed to synchronize {} with Flow Registry because could not determine the most recent version of the Flow in the Flow Registry", this, e);
+        }
     }
 
 
@@ -2945,12 +2965,12 @@ public final class StandardProcessGroup implements ProcessGroup {
             verifyCanUpdate(proposedSnapshot, true, verifyNotDirty);
 
             final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-            final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient());
+            final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), true);
 
             final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", versionedGroup);
             final ComparableDataFlow remoteFlow = new StandardComparableDataFlow("Remote Flow", proposedSnapshot.getFlowContents());
 
-            final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow);
+            final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow, new StaticDifferenceDescriptor());
             final FlowComparison flowComparison = flowComparator.compare();
 
             final Set<String> updatedVersionedComponentIds = flowComparison.getDifferences().stream()
@@ -2997,7 +3017,11 @@ public final class StandardProcessGroup implements ProcessGroup {
             .collect(Collectors.toSet());
 
         final Set<String> variablesRemoved = new HashSet<>(existingVariableNames);
-        variablesRemoved.removeAll(proposed.getVariables().keySet());
+
+        if (proposed.getVariables() != null) {
+            variablesRemoved.removeAll(proposed.getVariables().keySet());
+        }
+
         final Map<String, String> updatedVariableMap = new HashMap<>();
         variablesRemoved.forEach(var -> updatedVariableMap.put(var, null));
 
@@ -3017,12 +3041,25 @@ public final class StandardProcessGroup implements ProcessGroup {
             final String flowId = remoteCoordinates.getFlowId();
             final int version = remoteCoordinates.getVersion();
 
-            final VersionControlInformation vci = new StandardVersionControlInformation(registryId, bucketId, flowId, version, proposed, false, true);
+            final FlowRegistry flowRegistry = flowController.getFlowRegistryClient().getFlowRegistry(registryId);
+            final String registryName = flowRegistry == null ? registryId : flowRegistry.getName();
+
+            final VersionControlInformation vci = new StandardVersionControlInformation.Builder()
+                .registryId(registryId)
+                .registryName(registryName)
+                .bucketId(bucketId)
+                .bucketName(bucketId) // bucket name not yet known
+                .flowId(flowId)
+                .flowName(flowId) // flow id not yet known
+                .version(version)
+                .modified(false)
+                .current(true)
+                .build();
+
             group.setVersionControlInformation(vci, Collections.emptyMap());
         }
 
         // Child groups
-        // TODO: Need to take into account if child group is under version control pointing to a different Versioned Flow and if so need to handle it differently.
         final Map<String, ProcessGroup> childGroupsByVersionedId = group.getProcessGroups().stream()
             .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
         final Set<String> childGroupsRemoved = new HashSet<>(childGroupsByVersionedId.keySet());
@@ -3031,7 +3068,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final ProcessGroup childGroup = childGroupsByVersionedId.get(proposedChildGroup.getIdentifier());
 
             if (childGroup == null) {
-                final ProcessGroup added = addProcessGroup(proposedChildGroup, componentIdSeed);
+                final ProcessGroup added = addProcessGroup(group, proposedChildGroup, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else {
                 updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName);
@@ -3050,7 +3087,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedControllerService proposedService : proposed.getControllerServices()) {
             final ControllerServiceNode service = servicesByVersionedId.get(proposedService.getIdentifier());
             if (service == null) {
-                final ControllerServiceNode added = addControllerService(proposedService, componentIdSeed);
+                final ControllerServiceNode added = addControllerService(group, proposedService, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedService.getIdentifier())) {
                 updateControllerService(service, proposedService);
@@ -3069,7 +3106,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedFunnel proposedFunnel : proposed.getFunnels()) {
             final Funnel funnel = funnelsByVersionedId.get(proposedFunnel.getIdentifier());
             if (funnel == null) {
-                final Funnel added = addFunnel(proposedFunnel, componentIdSeed);
+                final Funnel added = addFunnel(group, proposedFunnel, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedFunnel.getIdentifier())) {
                 updateFunnel(funnel, proposedFunnel);
@@ -3090,7 +3127,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedPort proposedPort : proposed.getInputPorts()) {
             final Port port = inputPortsByVersionedId.get(proposedPort.getIdentifier());
             if (port == null) {
-                final Port added = addInputPort(proposedPort, componentIdSeed);
+                final Port added = addInputPort(group, proposedPort, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedPort.getIdentifier())) {
                 updatePort(port, proposedPort);
@@ -3110,7 +3147,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedPort proposedPort : proposed.getOutputPorts()) {
             final Port port = outputPortsByVersionedId.get(proposedPort.getIdentifier());
             if (port == null) {
-                final Port added = addOutputPort(proposedPort, componentIdSeed);
+                final Port added = addOutputPort(group, proposedPort, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedPort.getIdentifier())) {
                 updatePort(port, proposedPort);
@@ -3131,7 +3168,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedLabel proposedLabel : proposed.getLabels()) {
             final Label label = labelsByVersionedId.get(proposedLabel.getIdentifier());
             if (label == null) {
-                final Label added = addLabel(proposedLabel, componentIdSeed);
+                final Label added = addLabel(group, proposedLabel, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedLabel.getIdentifier())) {
                 updateLabel(label, proposedLabel);
@@ -3153,19 +3190,21 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedProcessor proposedProcessor : proposed.getProcessors()) {
             final ProcessorNode processor = processorsByVersionedId.get(proposedProcessor.getIdentifier());
             if (processor == null) {
-                final ProcessorNode added = addProcessor(proposedProcessor, componentIdSeed);
+                final ProcessorNode added = addProcessor(group, proposedProcessor, componentIdSeed);
 
-                final Set<Relationship> proposedAutoTerminated = proposedProcessor.getAutoTerminatedRelationships().stream()
-                    .map(relName -> added.getRelationship(relName))
-                    .collect(Collectors.toSet());
+                final Set<Relationship> proposedAutoTerminated =
+                    proposedProcessor.getAutoTerminatedRelationships() == null ? Collections.emptySet() : proposedProcessor.getAutoTerminatedRelationships().stream()
+                        .map(relName -> added.getRelationship(relName))
+                        .collect(Collectors.toSet());
                 autoTerminatedRelationships.put(added, proposedAutoTerminated);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedProcessor.getIdentifier())) {
                 updateProcessor(processor, proposedProcessor);
 
-                final Set<Relationship> proposedAutoTerminated = proposedProcessor.getAutoTerminatedRelationships().stream()
-                    .map(relName -> processor.getRelationship(relName))
-                    .collect(Collectors.toSet());
+                final Set<Relationship> proposedAutoTerminated =
+                    proposedProcessor.getAutoTerminatedRelationships() == null ? Collections.emptySet() : proposedProcessor.getAutoTerminatedRelationships().stream()
+                        .map(relName -> processor.getRelationship(relName))
+                        .collect(Collectors.toSet());
 
                 if (!processor.getAutoTerminatedRelationships().equals(proposedAutoTerminated)) {
                     autoTerminatedRelationships.put(processor, proposedAutoTerminated);
@@ -3188,7 +3227,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedRemoteProcessGroup proposedRpg : proposed.getRemoteProcessGroups()) {
             final RemoteProcessGroup rpg = rpgsByVersionedId.get(proposedRpg.getIdentifier());
             if (rpg == null) {
-                final RemoteProcessGroup added = addRemoteProcessGroup(proposedRpg, componentIdSeed);
+                final RemoteProcessGroup added = addRemoteProcessGroup(group, proposedRpg, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedRpg.getIdentifier())) {
                 updateRemoteProcessGroup(rpg, proposedRpg);
@@ -3209,7 +3248,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedConnection proposedConnection : proposed.getConnections()) {
             final Connection connection = connectionsByVersionedId.get(proposedConnection.getIdentifier());
             if (connection == null) {
-                final Connection added = addConnection(proposedConnection, componentIdSeed);
+                final Connection added = addConnection(group, proposedConnection, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (!connection.getSource().isRunning() && !connection.getDestination().isRunning()) {
                 // If the connection needs to be updated, then the source and destination will already have
@@ -3306,20 +3345,22 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
 
-    private ProcessGroup addProcessGroup(final VersionedProcessGroup proposed, final String componentIdSeed) throws ProcessorInstantiationException {
+    private ProcessGroup addProcessGroup(final ProcessGroup destination, final VersionedProcessGroup proposed, final String componentIdSeed) throws ProcessorInstantiationException {
         final ProcessGroup group = flowController.createProcessGroup(generateUuid(componentIdSeed));
         group.setVersionedComponentId(proposed.getIdentifier());
-        addProcessGroup(group);
+        group.setParent(destination);
         updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true);
+        destination.addProcessGroup(group);
         return group;
     }
 
     private void updateConnection(final Connection connection, final VersionedConnection proposed) {
-        connection.setBendPoints(proposed.getBends().stream()
-            .map(pos -> new Position(pos.getX(), pos.getY()))
-            .collect(Collectors.toList()));
+        connection.setBendPoints(proposed.getBends() == null ? Collections.emptyList() :
+            proposed.getBends().stream()
+                .map(pos -> new Position(pos.getX(), pos.getY()))
+                .collect(Collectors.toList()));
 
-        connection.setDestination(getConnectable(proposed.getDestination()));
+        connection.setDestination(getConnectable(connection.getProcessGroup(), proposed.getDestination()));
         connection.setLabelIndex(proposed.getLabelIndex());
         connection.setName(proposed.getName());
         connection.setRelationships(proposed.getSelectedRelationships().stream()
@@ -3332,7 +3373,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         queue.setBackPressureObjectThreshold(proposed.getBackPressureObjectThreshold());
         queue.setFlowFileExpiration(proposed.getFlowFileExpiration());
 
-        final List<FlowFilePrioritizer> prioritizers = proposed.getPrioritizers().stream()
+        final List<FlowFilePrioritizer> prioritizers = proposed.getPrioritizers() == null ? Collections.emptyList() : proposed.getPrioritizers().stream()
             .map(prioritizerName -> {
                 try {
                     return flowController.createPrioritizer(prioritizerName);
@@ -3345,14 +3386,14 @@ public final class StandardProcessGroup implements ProcessGroup {
         queue.setPriorities(prioritizers);
     }
 
-    private Connection addConnection(final VersionedConnection proposed, final String componentIdSeed) {
-        final Connectable source = getConnectable(proposed.getSource());
+    private Connection addConnection(final ProcessGroup destinationGroup, final VersionedConnection proposed, final String componentIdSeed) {
+        final Connectable source = getConnectable(destinationGroup, proposed.getSource());
         if (source == null) {
             throw new IllegalArgumentException("Connection has a source with identifier " + proposed.getIdentifier()
                 + " but no component could be found in the Process Group with a corresponding identifier");
         }
 
-        final Connectable destination = getConnectable(proposed.getDestination());
+        final Connectable destination = getConnectable(destinationGroup, proposed.getDestination());
         if (destination == null) {
             throw new IllegalArgumentException("Connection has a destination with identifier " + proposed.getIdentifier()
                 + " but no component could be found in the Process Group with a corresponding identifier");
@@ -3360,43 +3401,65 @@ public final class StandardProcessGroup implements ProcessGroup {
 
         final Connection connection = flowController.createConnection(generateUuid(componentIdSeed), proposed.getName(), source, destination, proposed.getSelectedRelationships());
         connection.setVersionedComponentId(proposed.getIdentifier());
-        addConnection(connection);
+        destinationGroup.addConnection(connection);
         updateConnection(connection, proposed);
 
         return connection;
     }
 
-    private Connectable getConnectable(final ConnectableComponent connectableComponent) {
+    private Connectable getConnectable(final ProcessGroup group, final ConnectableComponent connectableComponent) {
         final String id = connectableComponent.getId();
 
         switch (connectableComponent.getType()) {
             case FUNNEL:
-                return getFunnels().stream()
+                return group.getFunnels().stream()
                     .filter(component -> component.getVersionedComponentId().isPresent())
                     .filter(component -> id.equals(component.getVersionedComponentId().get()))
                     .findAny()
                     .orElse(null);
-            case INPUT_PORT:
-                return getInputPorts().stream()
+            case INPUT_PORT: {
+                final Optional<Port> port = group.getInputPorts().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny();
+
+                if (port.isPresent()) {
+                    return port.get();
+                }
+
+                return group.getProcessGroups().stream()
+                    .flatMap(gr -> gr.getInputPorts().stream())
                     .filter(component -> component.getVersionedComponentId().isPresent())
                     .filter(component -> id.equals(component.getVersionedComponentId().get()))
                     .findAny()
                     .orElse(null);
-            case OUTPUT_PORT:
-                return getOutputPorts().stream()
+            }
+            case OUTPUT_PORT: {
+                final Optional<Port> port = group.getOutputPorts().stream()
+                    .filter(component -> component.getVersionedComponentId().isPresent())
+                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .findAny();
+
+                if (port.isPresent()) {
+                    return port.get();
+                }
+
+                return group.getProcessGroups().stream()
+                    .flatMap(gr -> gr.getOutputPorts().stream())
                     .filter(component -> component.getVersionedComponentId().isPresent())
                     .filter(component -> id.equals(component.getVersionedComponentId().get()))
                     .findAny()
                     .orElse(null);
+            }
             case PROCESSOR:
-                return getProcessors().stream()
+                return group.getProcessors().stream()
                     .filter(component -> component.getVersionedComponentId().isPresent())
                     .filter(component -> id.equals(component.getVersionedComponentId().get()))
                     .findAny()
                     .orElse(null);
             case REMOTE_INPUT_PORT: {
                 final String rpgId = connectableComponent.getGroupId();
-                final Optional<RemoteProcessGroup> rpgOption = getRemoteProcessGroups().stream()
+                final Optional<RemoteProcessGroup> rpgOption = group.getRemoteProcessGroups().stream()
                     .filter(component -> component.getVersionedComponentId().isPresent())
                     .filter(component -> id.equals(component.getVersionedComponentId().get()))
                     .findAny();
@@ -3415,7 +3478,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
             case REMOTE_OUTPUT_PORT: {
                 final String rpgId = connectableComponent.getGroupId();
-                final Optional<RemoteProcessGroup> rpgOption = getRemoteProcessGroups().stream()
+                final Optional<RemoteProcessGroup> rpgOption = group.getRemoteProcessGroups().stream()
                     .filter(component -> component.getVersionedComponentId().isPresent())
                     .filter(component -> id.equals(component.getVersionedComponentId().get()))
                     .findAny();
@@ -3471,7 +3534,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         return new BundleCoordinate(bundle.getGroup(), bundle.getArtifact(), bundle.getVersion());
     }
 
-    private ControllerServiceNode addControllerService(final VersionedControllerService proposed, final String componentIdSeed) {
+    private ControllerServiceNode addControllerService(final ProcessGroup destination, final VersionedControllerService proposed, final String componentIdSeed) {
         final String type = proposed.getType();
         final String id = generateUuid(componentIdSeed);
 
@@ -3483,7 +3546,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         final ControllerServiceNode newService = flowController.createControllerService(type, id, coordinate, additionalUrls, firstTimeAdded);
         newService.setVersionedComponentId(proposed.getIdentifier());
 
-        addControllerService(newService);
+        destination.addControllerService(newService);
         updateControllerService(newService, proposed);
 
         return newService;
@@ -3493,10 +3556,10 @@ public final class StandardProcessGroup implements ProcessGroup {
         funnel.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
     }
 
-    private Funnel addFunnel(final VersionedFunnel proposed, final String componentIdSeed) {
+    private Funnel addFunnel(final ProcessGroup destination, final VersionedFunnel proposed, final String componentIdSeed) {
         final Funnel funnel = flowController.createFunnel(generateUuid(componentIdSeed));
         funnel.setVersionedComponentId(proposed.getIdentifier());
-        addFunnel(funnel);
+        destination.addFunnel(funnel);
         updateFunnel(funnel, proposed);
 
         return funnel;
@@ -3508,28 +3571,28 @@ public final class StandardProcessGroup implements ProcessGroup {
         port.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
     }
 
-    private Port addInputPort(final VersionedPort proposed, final String componentIdSeed) {
+    private Port addInputPort(final ProcessGroup destination, final VersionedPort proposed, final String componentIdSeed) {
         final Port port = flowController.createLocalInputPort(generateUuid(componentIdSeed), proposed.getName());
         port.setVersionedComponentId(proposed.getIdentifier());
-        addInputPort(port);
+        destination.addInputPort(port);
         updatePort(port, proposed);
 
         return port;
     }
 
-    private Port addOutputPort(final VersionedPort proposed, final String componentIdSeed) {
-        final Port port = flowController.createLocalInputPort(generateUuid(componentIdSeed), proposed.getName());
+    private Port addOutputPort(final ProcessGroup destination, final VersionedPort proposed, final String componentIdSeed) {
+        final Port port = flowController.createLocalOutputPort(generateUuid(componentIdSeed), proposed.getName());
         port.setVersionedComponentId(proposed.getIdentifier());
-        addOutputPort(port);
+        destination.addOutputPort(port);
         updatePort(port, proposed);
 
         return port;
     }
 
-    private Label addLabel(final VersionedLabel proposed, final String componentIdSeed) {
+    private Label addLabel(final ProcessGroup destination, final VersionedLabel proposed, final String componentIdSeed) {
         final Label label = flowController.createLabel(generateUuid(componentIdSeed), proposed.getLabel());
         label.setVersionedComponentId(proposed.getIdentifier());
-        addLabel(label);
+        destination.addLabel(label);
         updateLabel(label, proposed);
 
         return label;
@@ -3542,12 +3605,12 @@ public final class StandardProcessGroup implements ProcessGroup {
         label.setValue(proposed.getLabel());
     }
 
-    private ProcessorNode addProcessor(final VersionedProcessor proposed, final String componentIdSeed) throws ProcessorInstantiationException {
+    private ProcessorNode addProcessor(final ProcessGroup destination, final VersionedProcessor proposed, final String componentIdSeed) throws ProcessorInstantiationException {
         final BundleCoordinate coordinate = toCoordinate(proposed.getBundle());
         final ProcessorNode procNode = flowController.createProcessor(proposed.getType(), generateUuid(componentIdSeed), coordinate, true);
         procNode.setVersionedComponentId(proposed.getIdentifier());
 
-        addProcessor(procNode);
+        destination.addProcessor(procNode);
         updateProcessor(procNode, proposed);
 
         return procNode;
@@ -3584,15 +3647,18 @@ public final class StandardProcessGroup implements ProcessGroup {
             fullPropertyMap.put(property.getName(), null);
         }
 
-        fullPropertyMap.putAll(proposedProperties);
+        if (proposedProperties != null) {
+            fullPropertyMap.putAll(proposedProperties);
+        }
+
         return fullPropertyMap;
     }
 
-    private RemoteProcessGroup addRemoteProcessGroup(final VersionedRemoteProcessGroup proposed, final String componentIdSeed) {
+    private RemoteProcessGroup addRemoteProcessGroup(final ProcessGroup destination, final VersionedRemoteProcessGroup proposed, final String componentIdSeed) {
         final RemoteProcessGroup rpg = flowController.createRemoteProcessGroup(generateUuid(componentIdSeed), proposed.getTargetUris());
         rpg.setVersionedComponentId(proposed.getIdentifier());
 
-        addRemoteProcessGroup(rpg);
+        destination.addRemoteProcessGroup(rpg);
         updateRemoteProcessGroup(rpg, proposed);
 
         return rpg;
@@ -3601,12 +3667,12 @@ public final class StandardProcessGroup implements ProcessGroup {
     private void updateRemoteProcessGroup(final RemoteProcessGroup rpg, final VersionedRemoteProcessGroup proposed) {
         rpg.setComments(proposed.getComments());
         rpg.setCommunicationsTimeout(proposed.getCommunicationsTimeout());
-        rpg.setInputPorts(proposed.getInputPorts().stream()
+        rpg.setInputPorts(proposed.getInputPorts() == null ? Collections.emptySet() : proposed.getInputPorts().stream()
             .map(port -> createPortDescriptor(port))
             .collect(Collectors.toSet()));
         rpg.setName(proposed.getName());
         rpg.setNetworkInterface(proposed.getLocalNetworkInterface());
-        rpg.setOutputPorts(proposed.getOutputPorts().stream()
+        rpg.setOutputPorts(proposed.getOutputPorts() == null ? Collections.emptySet() : proposed.getOutputPorts().stream()
             .map(port -> createPortDescriptor(port))
             .collect(Collectors.toSet()));
         rpg.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
@@ -3633,7 +3699,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
 
-    public Optional<Boolean> isModified() {
+    private Set<FlowDifference> getModifications() {
         final StandardVersionControlInformation vci = versionControlInfo.get();
 
         // If this group is not under version control, then we need to notify the parent
@@ -3645,17 +3711,17 @@ public final class StandardProcessGroup implements ProcessGroup {
         // say that the flow is modified. There would be no way to ever go back to the flow not being modified.
         // So we have to perform a diff of the flows and see if they are the same.
         if (vci == null) {
-            return Optional.of(Boolean.FALSE);
+            return null;
         }
 
         if (vci.getFlowSnapshot() == null) {
             // we haven't retrieved the flow from the Flow Registry yet, so we don't know if it's been modified.
             // As a result, we will just return an empty optional
-            return Optional.empty();
+            return null;
         }
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient());
+        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), true);
 
         final ComparableDataFlow currentFlow = new ComparableDataFlow() {
             @Override
@@ -3681,17 +3747,16 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
         };
 
-        final FlowComparator flowComparator = new StandardFlowComparator(currentFlow, snapshotFlow);
+        final FlowComparator flowComparator = new StandardFlowComparator(currentFlow, snapshotFlow, new EvolvingDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();
         final Set<FlowDifference> differences = comparison.getDifferences();
-        final boolean modified = differences.stream()
+        final Set<FlowDifference> functionalDifferences = differences.stream()
             .filter(diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED)
             .filter(diff -> diff.getDifferenceType() != DifferenceType.STYLE_CHANGED)
-            .findAny()
-            .isPresent();
+            .collect(Collectors.toSet());
 
-        LOG.debug("There are {} differences between this Local FLow and the Versioned Flow: {}", differences.size(), differences);
-        return Optional.of(modified);
+        LOG.debug("There are {} differences between this Local Flow and the Versioned Flow: {}", differences.size(), differences);
+        return functionalDifferences;
     }
 
 
@@ -3713,10 +3778,21 @@ public final class StandardProcessGroup implements ProcessGroup {
                             + " synched with the Flow Registry before continuing. This will happen periodically in the background, so please try the request again later");
                     }
 
+                    final Set<FlowDifference> modifications = getModifications();
+
                     if (Boolean.TRUE.equals(modifiedOption.get())) {
+                        final String changes = modifications.stream()
+                            .map(FlowDifference::toString)
+                            .collect(Collectors.joining("\n"));
+
+                        LOG.error("Cannot change the Version of the flow for {} because the Process Group has been modified ({} modifications) "
+                            + "since it was last synchronized with the Flow Registry. The following differences were found:\n{}",
+                            this, modifications.size(), changes);
+
                         throw new IllegalStateException("Cannot change the Version of the flow for " + this
-                            + " because the Process Group has been modified since it was last synchronized with the Flow Registry. The Process Group must be"
-                            + " restored to its original form before changing the version");
+                            + " because the Process Group has been modified (" + modifications.size()
+                            + " modifications) since it was last synchronized with the Flow Registry. The Process Group must be"
+                            + " reverted to its original form before changing the version. See logs for more information on what has changed.");
                     }
                 }
             }
@@ -3851,11 +3927,13 @@ public final class StandardProcessGroup implements ProcessGroup {
                 .forEach(conn -> proposedConnections.remove(conn.getVersionedComponentId().get()));
 
             for (final VersionedConnection connectionToAdd : proposedConnections.values()) {
-                for (final String prioritizerType : connectionToAdd.getPrioritizers()) {
-                    try {
-                        flowController.createPrioritizer(prioritizerType);
-                    } catch (Exception e) {
-                        throw new IllegalArgumentException("Unable to create Prioritizer of type " + prioritizerType, e);
+                if (connectionToAdd.getPrioritizers() != null) {
+                    for (final String prioritizerType : connectionToAdd.getPrioritizers()) {
+                        try {
+                            flowController.createPrioritizer(prioritizerType);
+                        } catch (Exception e) {
+                            throw new IllegalArgumentException("Unable to create Prioritizer of type " + prioritizerType, e);
+                        }
                     }
                 }
             }


[02/50] nifi git commit: NIFI-4436: - Added the import dialog for importing a versioned flow into a new process group. - Added the change version dialog for upgrading/downgrading a versioned flow.

Posted by bb...@apache.org.
NIFI-4436:
- Added the import dialog for importing a versioned flow into a new process group.
- Added the change version dialog for upgrading/downgrading a versioned flow.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/696d583b
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/696d583b
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/696d583b

Branch: refs/heads/master
Commit: 696d583b148b60d41853add8d3e2d24b4ff907f3
Parents: 6aa8b5c
Author: Matt Gilman <ma...@gmail.com>
Authored: Wed Nov 1 17:21:04 2017 -0400
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:52 2018 -0500

----------------------------------------------------------------------
 .../nifi/web/api/dto/FlowConfigurationDTO.java  |   14 -
 .../api/dto/VersionControlInformationDTO.java   |   10 +
 .../nifi/web/api/entity/CurrentUserEntity.java  |   14 +
 .../StartVersionControlRequestEntity.java       |   48 +
 .../web/api/entity/VersionedFlowEntity.java     |   11 -
 .../VersionedFlowSnapshotMetadataEntity.java    |   47 +
 .../VersionedFlowSnapshotMetadataSetEntity.java |   38 +
 .../web/api/entity/VersionedFlowsEntity.java    |   38 +
 .../org/apache/nifi/web/NiFiServiceFacade.java  |   62 +-
 .../nifi/web/StandardNiFiServiceFacade.java     |  158 ++-
 .../apache/nifi/web/api/ControllerResource.java |   67 +-
 .../org/apache/nifi/web/api/FlowResource.java   |  128 ++-
 .../apache/nifi/web/api/VersionsResource.java   |   91 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |    5 +-
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |    7 +
 .../org/apache/nifi/web/dao/RegistryDAO.java    |   16 +-
 .../nifi/web/dao/impl/FlowRegistryDAO.java      |   59 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |   22 +
 .../src/main/resources/nifi-web-api-context.xml |    2 -
 .../src/main/webapp/WEB-INF/pages/canvas.jsp    |    1 +
 .../canvas/import-flow-version-dialog.jsp       |   44 +
 .../WEB-INF/partials/canvas/navigation.jsp      |    4 +-
 .../canvas/new-process-group-dialog.jsp         |    3 +
 .../canvas/new-remote-process-group-dialog.jsp  |    2 +-
 .../canvas/registry-configuration-dialog.jsp    |    4 +-
 .../canvas/save-flow-version-dialog.jsp         |   20 +-
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |   18 +-
 .../header/components/nf-ng-group-component.js  |   32 +-
 .../src/main/webapp/js/nf/canvas/nf-actions.js  |   23 +-
 .../webapp/js/nf/canvas/nf-canvas-bootstrap.js  |    3 +-
 .../main/webapp/js/nf/canvas/nf-canvas-utils.js |    7 -
 .../src/main/webapp/js/nf/canvas/nf-canvas.js   |   18 -
 .../main/webapp/js/nf/canvas/nf-context-menu.js |   16 +-
 .../main/webapp/js/nf/canvas/nf-flow-version.js | 1036 +++++++++++++++---
 .../src/main/webapp/js/nf/canvas/nf-settings.js |   23 +-
 .../src/main/webapp/js/nf/nf-common.js          |   11 +
 36 files changed, 1698 insertions(+), 404 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
index 03e1a7d..d7a0c83 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
@@ -32,7 +32,6 @@ public class FlowConfigurationDTO {
     private Boolean supportsManagedAuthorizer;
     private Boolean supportsConfigurableAuthorizer;
     private Boolean supportsConfigurableUsersAndGroups;
-    private Boolean supportsFlowVersioning;
     private Long autoRefreshIntervalSeconds;
 
     private Date currentTime;
@@ -129,17 +128,4 @@ public class FlowConfigurationDTO {
         this.timeOffset = timeOffset;
     }
 
-    /**
-     * @return whether this NiFi is configured for support flow versioning
-     */
-    @ApiModelProperty(
-            value = "Whether this NiFi supports flow versioning."
-    )
-    public Boolean getSupportsFlowVersioning() {
-        return supportsFlowVersioning;
-    }
-
-    public void setSupportsFlowVersioning(Boolean supportsFlowVersioning) {
-        this.supportsFlowVersioning = supportsFlowVersioning;
-    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
index d27e830..e9aa246 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
@@ -27,6 +27,7 @@ public class VersionControlInformationDTO {
     private String registryId;
     private String bucketId;
     private String flowId;
+    private String flowName;
     private Integer version;
     private Boolean modified;
     private Boolean current;
@@ -67,6 +68,15 @@ public class VersionControlInformationDTO {
         this.flowId = flowId;
     }
 
+    @ApiModelProperty("The name of the flow")
+    public String getFlowName() {
+        return flowName;
+    }
+
+    public void setFlowName(String flowName) {
+        this.flowName = flowName;
+    }
+
     @ApiModelProperty("The version of the flow")
     public Integer getVersion() {
         return version;

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java
index 6b8e28f..8121ce4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java
@@ -38,6 +38,8 @@ public class CurrentUserEntity extends Entity {
     private PermissionsDTO systemPermissions;
     private PermissionsDTO restrictedComponentsPermissions;
 
+    private boolean canVersionFlows;
+
     /**
      * @return the user identity being serialized
      */
@@ -145,4 +147,16 @@ public class CurrentUserEntity extends Entity {
     public void setRestrictedComponentsPermissions(PermissionsDTO restrictedComponentsPermissions) {
         this.restrictedComponentsPermissions = restrictedComponentsPermissions;
     }
+
+    /**
+     * @return whether the current user can version flows
+     */
+    @ApiModelProperty("Whether the current user can version flows.")
+    public boolean isCanVersionFlows() {
+        return canVersionFlows;
+    }
+
+    public void setCanVersionFlows(boolean canVersionFlows) {
+        this.canVersionFlows = canVersionFlows;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
new file mode 100644
index 0000000..0876140
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
@@ -0,0 +1,48 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.VersionedFlowDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "versionedFlow")
+public class StartVersionControlRequestEntity extends Entity {
+    private VersionedFlowDTO versionedFlow;
+    private RevisionDTO processGroupRevision;
+
+    @ApiModelProperty("The versioned flow")
+    public VersionedFlowDTO getVersionedFlow() {
+        return versionedFlow;
+    }
+
+    public void setVersionedFlow(VersionedFlowDTO versionedFLow) {
+        this.versionedFlow = versionedFLow;
+    }
+
+    @ApiModelProperty("The Revision of the Process Group under Version Control")
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(final RevisionDTO revision) {
+        this.processGroupRevision = revision;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
index b94255a..4a43826 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
@@ -18,7 +18,6 @@
 package org.apache.nifi.web.api.entity;
 
 import io.swagger.annotations.ApiModelProperty;
-import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 
 import javax.xml.bind.annotation.XmlRootElement;
@@ -26,7 +25,6 @@ import javax.xml.bind.annotation.XmlRootElement;
 @XmlRootElement(name = "versionedFlow")
 public class VersionedFlowEntity extends Entity {
     private VersionedFlowDTO versionedFlow;
-    private RevisionDTO processGroupRevision;
 
     @ApiModelProperty("The versioned flow")
     public VersionedFlowDTO getVersionedFlow() {
@@ -36,13 +34,4 @@ public class VersionedFlowEntity extends Entity {
     public void setVersionedFlow(VersionedFlowDTO versionedFLow) {
         this.versionedFlow = versionedFLow;
     }
-
-    @ApiModelProperty("The Revision of the Process Group under Version Control")
-    public RevisionDTO getProcessGroupRevision() {
-        return processGroupRevision;
-    }
-
-    public void setProcessGroupRevision(final RevisionDTO revision) {
-        this.processGroupRevision = revision;
-    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
new file mode 100644
index 0000000..29b57cc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "versionedFlowSnapshotMetadata")
+public class VersionedFlowSnapshotMetadataEntity extends Entity {
+    private VersionedFlowSnapshotMetadata versionedFlowSnapshotMetadata;
+    private String registryId;
+
+    @ApiModelProperty("The collection of versioned flow snapshot metadata")
+    public VersionedFlowSnapshotMetadata getVersionedFlowSnapshotMetadata() {
+        return versionedFlowSnapshotMetadata;
+    }
+
+    public void setVersionedFlowMetadata(VersionedFlowSnapshotMetadata versionedFlowSnapshotMetadata) {
+        this.versionedFlowSnapshotMetadata = versionedFlowSnapshotMetadata;
+    }
+
+    @ApiModelProperty("The ID of the Registry that this flow belongs to")
+    public String getRegistryId() {
+        return registryId;
+    }
+
+    public void setRegistryId(String registryId) {
+        this.registryId = registryId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataSetEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataSetEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataSetEntity.java
new file mode 100644
index 0000000..d49b40f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataSetEntity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.web.api.entity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
+
+@XmlRootElement(name = "versionedFlowSnapshotMetadataSetEntity")
+public class VersionedFlowSnapshotMetadataSetEntity extends Entity {
+
+    private Set<VersionedFlowSnapshotMetadataEntity> versionedFlowSnapshotMetadataSet;
+
+    /**
+     * @return collection of VersionedFlowSnapshotMetadataEntity's that are being serialized
+     */
+    public Set<VersionedFlowSnapshotMetadataEntity> getVersionedFlowSnapshotMetadataSet() {
+        return versionedFlowSnapshotMetadataSet;
+    }
+
+    public void setVersionedFlowSnapshotMetadataSet(Set<VersionedFlowSnapshotMetadataEntity> versionedFlowSnapshotMetadataSet) {
+        this.versionedFlowSnapshotMetadataSet = versionedFlowSnapshotMetadataSet;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowsEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowsEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowsEntity.java
new file mode 100644
index 0000000..1104eae
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowsEntity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.web.api.entity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
+
+@XmlRootElement(name = "versionedFlowsEntity")
+public class VersionedFlowsEntity extends Entity {
+
+    private Set<VersionedFlowEntity> versionedFlows;
+
+    /**
+     * @return collection of VersionedEntity's that are being serialized
+     */
+    public Set<VersionedFlowEntity> getVersionedFlows() {
+        return versionedFlows;
+    }
+
+    public void setVersionedFlows(Set<VersionedFlowEntity> versionedFlows) {
+        this.versionedFlows = versionedFlows;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index d851677..e299059 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -77,6 +77,7 @@ import org.apache.nifi.web.api.entity.AccessPolicyEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.BucketEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
 import org.apache.nifi.web.api.entity.ConnectionStatusEntity;
@@ -103,6 +104,7 @@ import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
 import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
 import org.apache.nifi.web.api.entity.SnippetEntity;
+import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.StatusHistoryEntity;
 import org.apache.nifi.web.api.entity.TemplateEntity;
 import org.apache.nifi.web.api.entity.UserEntity;
@@ -111,6 +113,7 @@ import org.apache.nifi.web.api.entity.VariableRegistryEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 
 import java.io.IOException;
 import java.util.Date;
@@ -1300,7 +1303,7 @@ public interface NiFiServiceFacade {
      * @return a VersionControlComponentMappingEntity that contains the information needed to notify a Process Group where it is tracking to and map
      *         component ID's to their Versioned Component ID's
      */
-    VersionControlComponentMappingEntity registerFlowWithFlowRegistry(String groupId, VersionedFlowEntity requestEntity);
+    VersionControlComponentMappingEntity registerFlowWithFlowRegistry(String groupId, StartVersionControlRequestEntity requestEntity);
 
     /**
      * Adds the given snapshot to the already existing Versioned Flow, which resides in the given Flow Registry with the given id
@@ -1854,7 +1857,7 @@ public interface NiFiServiceFacade {
      * @param registryDTO The registry DTO
      * @return The reporting task DTO
      */
-    RegistryEntity createRegistry(Revision revision, RegistryDTO registryDTO);
+    RegistryEntity createRegistryClient(Revision revision, RegistryDTO registryDTO);
 
     /**
      * Gets a registry with the specified id.
@@ -1862,14 +1865,52 @@ public interface NiFiServiceFacade {
      * @param registryId id
      * @return entity
      */
-    RegistryEntity getRegistry(String registryId);
+    RegistryEntity getRegistryClient(String registryId);
 
     /**
-     * Gets all registries.
+     * Returns all registry clients.
      *
+     * @return registry clients
+     */
+    Set<RegistryEntity> getRegistryClients();
+
+    /**
+     * Gets all registries for the current user.
+     *
+     * @param user current user
      * @return registries
      */
-    Set<RegistryEntity> getRegistries();
+    Set<RegistryEntity> getRegistriesForUser(NiFiUser user);
+
+    /**
+     * Gets all buckets for a given registry.
+     *
+     * @param registryId registry id
+     * @param user current user
+     * @return the buckets
+     */
+    Set<BucketEntity> getBucketsForUser(String registryId, NiFiUser user);
+
+    /**
+     * Gets the flows for the current user for the specified registry and bucket.
+     *
+     * @param registryId registry id
+     * @param bucketId bucket id
+     * @param user current user
+     * @return the flows
+     */
+    Set<VersionedFlowEntity> getFlowsForUser(String registryId, String bucketId, NiFiUser user);
+
+    /**
+     * Gets the versions of the specified registry, bucket, and flow for the current user.
+     *
+     * @param registryId registry id
+     * @param bucketId bucket id
+     * @param flowId flow id
+     * @param user current user
+     * @return the versions of the flow
+     */
+    Set<VersionedFlowSnapshotMetadataEntity> getFlowVersionsForUser(String registryId, String bucketId, String flowId, NiFiUser user);
 
     /**
      * Updates the specified registry using the specified revision.
@@ -1878,7 +1919,7 @@ public interface NiFiServiceFacade {
      * @param registryDTO the registry dto
      * @return the updated registry registry entity
      */
-    RegistryEntity updateRegistry(Revision revision, RegistryDTO registryDTO);
+    RegistryEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO);
 
     /**
      * Deletes the specified registry using the specified revision.
@@ -1887,7 +1928,14 @@ public interface NiFiServiceFacade {
      * @param registryId id
      * @return the deleted registry entity
      */
-    RegistryEntity deleteRegistry(Revision revision, String registryId);
+    RegistryEntity deleteRegistryClient(Revision revision, String registryId);
+
+    /**
+     * Verifies the specified registry can be removed.
+     *
+     * @param registryId the registry id
+     */
+    void verifyDeleteRegistry(String registryId);
 
     // ----------------------------------------
     // History methods

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 2b5b5c3..a319f27 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,32 +16,8 @@
  */
 package org.apache.nifi.web;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
-
+import com.google.common.collect.Sets;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
 import org.apache.nifi.action.FlowChangeAction;
@@ -111,15 +87,17 @@ import org.apache.nifi.history.History;
 import org.apache.nifi.history.HistoryQuery;
 import org.apache.nifi.history.PreviousValue;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.UnknownResourceException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.DifferenceType;
@@ -144,6 +122,7 @@ import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.api.dto.AccessPolicyDTO;
 import org.apache.nifi.web.api.dto.AccessPolicySummaryDTO;
 import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.BucketDTO;
 import org.apache.nifi.web.api.dto.BulletinBoardDTO;
 import org.apache.nifi.web.api.dto.BulletinDTO;
 import org.apache.nifi.web.api.dto.BulletinQueryDTO;
@@ -215,6 +194,7 @@ import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.BucketEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
@@ -253,7 +233,9 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.dao.AccessPolicyDAO;
 import org.apache.nifi.web.dao.ConnectionDAO;
@@ -282,7 +264,30 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Sets;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Implementation of NiFiServiceFacade that performs revision checking.
@@ -2268,7 +2273,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
 
     @Override
-    public RegistryEntity createRegistry(Revision revision, RegistryDTO registryDTO) {
+    public RegistryEntity createRegistryClient(Revision revision, RegistryDTO registryDTO) {
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
         // read lock on the containing group
@@ -2292,7 +2297,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public RegistryEntity getRegistry(final String registryId) {
+    public RegistryEntity getRegistryClient(final String registryId) {
         final FlowRegistry registry = registryDAO.getFlowRegistry(registryId);
         return createRegistryEntity(registry);
     }
@@ -2319,15 +2324,90 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entity;
     }
 
+    private BucketEntity createBucketEntity(final Bucket bucket) {
+        if (bucket == null) {
+            return null;
+        }
+
+        final BucketDTO dto = new BucketDTO();
+        dto.setId(bucket.getIdentifier());
+        dto.setName(bucket.getName());
+        dto.setDescription(bucket.getDescription());
+        dto.setCreated(bucket.getCreatedTimestamp());
+
+        final BucketEntity entity = new BucketEntity();
+        entity.setBucket(dto);
+
+        return entity;
+    }
+
+    private VersionedFlowEntity createVersionedFlowEntity(final String registryId, final VersionedFlow versionedFlow) {
+        if (versionedFlow == null) {
+            return null;
+        }
+
+        final VersionedFlowDTO dto = new VersionedFlowDTO();
+        dto.setRegistryId(registryId);
+        dto.setBucketId(versionedFlow.getBucketIdentifier());
+        dto.setFlowId(versionedFlow.getIdentifier());
+        dto.setFlowName(versionedFlow.getName());
+        dto.setDescription(versionedFlow.getDescription());
+
+        final VersionedFlowEntity entity = new VersionedFlowEntity();
+        entity.setVersionedFlow(dto);
+
+        return entity;
+    }
+
+    private VersionedFlowSnapshotMetadataEntity createVersionedFlowSnapshotMetadataEntity(final String registryId, final VersionedFlowSnapshotMetadata metadata) {
+        if (metadata == null) {
+            return null;
+        }
+
+        final VersionedFlowSnapshotMetadataEntity entity = new VersionedFlowSnapshotMetadataEntity();
+        entity.setRegistryId(registryId);
+        entity.setVersionedFlowMetadata(metadata);
+
+        return entity;
+    }
+
     @Override
-    public Set<RegistryEntity> getRegistries() {
+    public Set<RegistryEntity> getRegistryClients() {
         return registryDAO.getFlowRegistries().stream()
             .map(this::createRegistryEntity)
             .collect(Collectors.toSet());
     }
 
     @Override
-    public RegistryEntity updateRegistry(Revision revision, RegistryDTO registryDTO) {
+    public Set<RegistryEntity> getRegistriesForUser(final NiFiUser user) {
+        return registryDAO.getFlowRegistriesForUser(user).stream()
+                .map(this::createRegistryEntity)
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<BucketEntity> getBucketsForUser(final String registryId, final NiFiUser user) {
+        return registryDAO.getBucketsForUser(registryId, user).stream()
+                .map(this::createBucketEntity)
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<VersionedFlowEntity> getFlowsForUser(String registryId, String bucketId, NiFiUser user) {
+        return registryDAO.getFlowsForUser(registryId, bucketId, user).stream()
+                .map(vf -> createVersionedFlowEntity(registryId, vf))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<VersionedFlowSnapshotMetadataEntity> getFlowVersionsForUser(String registryId, String bucketId, String flowId, NiFiUser user) {
+        return registryDAO.getFlowVersionsForUser(registryId, bucketId, flowId, user).stream()
+                .map(md -> createVersionedFlowSnapshotMetadataEntity(registryId, md))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public RegistryEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO) {
         final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
@@ -2350,7 +2430,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public RegistryEntity deleteRegistry(final Revision revision, final String registryId) {
+    public void verifyDeleteRegistry(String registryId) {
+        processGroupDAO.verifyDeleteFlowRegistry(registryId);
+    }
+
+    @Override
+    public RegistryEntity deleteRegistryClient(final Revision revision, final String registryId) {
         final RevisionClaim claim = new StandardRevisionClaim(revision);
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
@@ -3340,6 +3425,10 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         entity.setPoliciesPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getPolicies()));
         entity.setSystemPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getSystem()));
         entity.setRestrictedComponentsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getRestrictedComponents()));
+
+        // TODO - update to be user specific
+        entity.setCanVersionFlows(CollectionUtils.isNotEmpty(flowRegistryClient.getRegistryIdentifiers()));
+
         return entity;
     }
 
@@ -3538,7 +3627,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public VersionControlComponentMappingEntity registerFlowWithFlowRegistry(final String groupId, final VersionedFlowEntity requestEntity) {
+    public VersionControlComponentMappingEntity registerFlowWithFlowRegistry(final String groupId, final StartVersionControlRequestEntity requestEntity) {
         // Create a VersionedProcessGroup snapshot of the flow as it is currently.
         final InstantiatedVersionedProcessGroup versionedProcessGroup = createFlowSnapshot(groupId);
 
@@ -3584,6 +3673,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         vci.setBucketId(registeredFlow.getBucketIdentifier());
         vci.setCurrent(true);
         vci.setFlowId(registeredFlow.getIdentifier());
+        vci.setFlowName(registeredFlow.getName());
         vci.setGroupId(groupId);
         vci.setModified(false);
         vci.setRegistryId(registryId);

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
index 959d06d..356d231 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
@@ -46,6 +46,7 @@ import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.Entity;
 import org.apache.nifi.web.api.entity.HistoryEntity;
 import org.apache.nifi.web.api.entity.NodeEntity;
+import org.apache.nifi.web.api.entity.RegistriesEntity;
 import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
@@ -69,6 +70,7 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.net.URI;
 import java.util.Date;
+import java.util.Set;
 
 /**
  * RESTful endpoint for managing a Flow Controller.
@@ -93,7 +95,7 @@ public class ControllerResource extends ApplicationResource {
      * @return dtos
      */
     public RegistryEntity populateRemainingRegistryEntityContent(final RegistryEntity registryEntity) {
-        registryEntity.setUri(generateResourceUri("controller", "registries", registryEntity.getId()));
+        registryEntity.setUri(generateResourceUri("controller", "registry-clients", registryEntity.getId()));
         return registryEntity;
     }
 
@@ -310,6 +312,36 @@ public class ControllerResource extends ApplicationResource {
     // registries
     // ----------
 
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("registry-clients")
+    @ApiOperation(value = "Gets the listing of available registry clients", response = RegistriesEntity.class, authorizations = {
+            @Authorization(value = "Read - /flow")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+            @ApiResponse(code = 401, message = "Client could not be authenticated."),
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 404, message = "The specified resource could not be found."),
+            @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getRegistryClients() {
+        authorizeController(RequestAction.READ);
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        final Set<RegistryEntity> registries = serviceFacade.getRegistryClients();
+        registries.forEach(registry -> populateRemainingRegistryEntityContent(registry));
+
+        final RegistriesEntity registryEntities = new RegistriesEntity();
+        registryEntities.setRegistries(registries);
+
+        return generateOkResponse(registryEntities).build();
+    }
+
     /**
      * Creates a new Registry.
      *
@@ -320,9 +352,9 @@ public class ControllerResource extends ApplicationResource {
     @POST
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("registries")
+    @Path("registry-clients")
     @ApiOperation(
-            value = "Creates a new registry",
+            value = "Creates a new registry client",
             response = RegistryEntity.class,
             authorizations = {
                     @Authorization(value = "Write - /controller")
@@ -336,8 +368,7 @@ public class ControllerResource extends ApplicationResource {
                     @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
             }
     )
-
-    public Response createRegistry(
+    public Response createRegistryClient(
             @Context final HttpServletRequest httpServletRequest,
             @ApiParam(
                     value = "The registry configuration details.",
@@ -376,7 +407,7 @@ public class ControllerResource extends ApplicationResource {
 
                     // create the reporting task and generate the json
                     final Revision revision = getRevision(registryEntity, registry.getId());
-                    final RegistryEntity entity = serviceFacade.createRegistry(revision, registry);
+                    final RegistryEntity entity = serviceFacade.createRegistryClient(revision, registry);
                     populateRemainingRegistryEntityContent(entity);
 
                     // build the response
@@ -394,9 +425,9 @@ public class ControllerResource extends ApplicationResource {
     @GET
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/registries/{id}")
+    @Path("/registry-clients/{id}")
     @ApiOperation(
-            value = "Gets a registry",
+            value = "Gets a registry client",
             response = RegistryEntity.class,
             authorizations = {
                     @Authorization(value = "Read - /controller")
@@ -426,7 +457,7 @@ public class ControllerResource extends ApplicationResource {
         authorizeController(RequestAction.READ);
 
         // get the registry
-        final RegistryEntity entity = serviceFacade.getRegistry(id);
+        final RegistryEntity entity = serviceFacade.getRegistryClient(id);
         populateRemainingRegistryEntityContent(entity);
 
         return generateOkResponse(entity).build();
@@ -443,9 +474,9 @@ public class ControllerResource extends ApplicationResource {
     @PUT
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/registries/{id}")
+    @Path("/registry-clients/{id}")
     @ApiOperation(
-            value = "Updates a registry",
+            value = "Updates a registry client",
             response = RegistryEntity.class,
             authorizations = {
                     @Authorization(value = "Write - /controller")
@@ -460,7 +491,7 @@ public class ControllerResource extends ApplicationResource {
                     @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
             }
     )
-    public Response updateControllerService(
+    public Response updateRegistryClient(
             @Context HttpServletRequest httpServletRequest,
             @ApiParam(
                     value = "The registry id.",
@@ -505,7 +536,7 @@ public class ControllerResource extends ApplicationResource {
                     final RegistryDTO registry = registryEntity.getComponent();
 
                     // update the controller service
-                    final RegistryEntity entity = serviceFacade.updateRegistry(revision, registry);
+                    final RegistryEntity entity = serviceFacade.updateRegistryClient(revision, registry);
                     populateRemainingRegistryEntityContent(entity);
 
                     return generateOkResponse(entity).build();
@@ -528,9 +559,9 @@ public class ControllerResource extends ApplicationResource {
     @DELETE
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("/registries/{id}")
+    @Path("/registry-clients/{id}")
     @ApiOperation(
-            value = "Deletes a reistry",
+            value = "Deletes a registry client",
             response = RegistryEntity.class,
             authorizations = {
                     @Authorization(value = "Write - /controller")
@@ -545,7 +576,7 @@ public class ControllerResource extends ApplicationResource {
                     @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
             }
     )
-    public Response deleteRegistry(
+    public Response deleteRegistryClient(
             @Context HttpServletRequest httpServletRequest,
             @ApiParam(
                     value = "The revision is used to verify the client is working with the latest version of the flow.",
@@ -579,10 +610,10 @@ public class ControllerResource extends ApplicationResource {
                 lookup -> {
                     authorizeController(RequestAction.WRITE);
                 },
-                null,
+                () -> serviceFacade.verifyDeleteRegistry(id),
                 (revision, registryEntity) -> {
                     // delete the specified registry
-                    final RegistryEntity entity = serviceFacade.deleteRegistry(revision, registryEntity.getId());
+                    final RegistryEntity entity = serviceFacade.deleteRegistryClient(revision, registryEntity.getId());
                     return generateOkResponse(entity).build();
                 }
         );

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index 3e9be6d..b8bdc14 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -39,18 +39,13 @@ import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.nar.NarClassLoaders;
-import org.apache.nifi.registry.bucket.Bucket;
-import org.apache.nifi.registry.flow.FlowRegistry;
-import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.IllegalClusterResourceRequestException;
-import org.apache.nifi.web.NiFiCoreException;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.dto.AboutDTO;
 import org.apache.nifi.web.api.dto.BannerDTO;
-import org.apache.nifi.web.api.dto.BucketDTO;
 import org.apache.nifi.web.api.dto.BulletinBoardDTO;
 import org.apache.nifi.web.api.dto.BulletinQueryDTO;
 import org.apache.nifi.web.api.dto.ClusterDTO;
@@ -102,6 +97,10 @@ import org.apache.nifi.web.api.entity.SearchResultsEntity;
 import org.apache.nifi.web.api.entity.StatusHistoryEntity;
 import org.apache.nifi.web.api.entity.TemplateEntity;
 import org.apache.nifi.web.api.entity.TemplatesEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataSetEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowsEntity;
 import org.apache.nifi.web.api.request.BulletinBoardPatternParameter;
 import org.apache.nifi.web.api.request.DateTimeParameter;
 import org.apache.nifi.web.api.request.IntegerParameter;
@@ -121,7 +120,6 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.EnumSet;
@@ -158,11 +156,8 @@ public class FlowResource extends ApplicationResource {
     private TemplateResource templateResource;
     private ProcessGroupResource processGroupResource;
     private ControllerServiceResource controllerServiceResource;
-    private ControllerResource controllerResource;
     private ReportingTaskResource reportingTaskResource;
 
-    private FlowRegistryClient flowRegistryClient;
-
     public FlowResource() {
         super();
     }
@@ -1351,12 +1346,7 @@ public class FlowResource extends ApplicationResource {
     public Response getRegistries() {
         authorizeFlow();
 
-        if (isReplicateRequest()) {
-            return replicate(HttpMethod.GET);
-        }
-
-        final Set<RegistryEntity> registries = serviceFacade.getRegistries();
-        registries.forEach(registry -> controllerResource.populateRemainingRegistryEntityContent(registry));
+        final Set<RegistryEntity> registries = serviceFacade.getRegistriesForUser(NiFiUserUtils.getNiFiUser());
 
         final RegistriesEntity registryEntities = new RegistriesEntity();
         registryEntities.setRegistries(registries);
@@ -1387,39 +1377,89 @@ public class FlowResource extends ApplicationResource {
 
         authorizeFlow();
 
-        try {
-            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(id);
-            if (flowRegistry == null) {
-                throw new IllegalArgumentException("The specified registry id is unknown to this NiFi.");
-            }
+        final Set<BucketEntity> buckets = serviceFacade.getBucketsForUser(id, NiFiUserUtils.getNiFiUser());
 
-            final Set<Bucket> userBuckets = flowRegistry.getBuckets(NiFiUserUtils.getNiFiUser());
+        final BucketsEntity bucketsEntity = new BucketsEntity();
+        bucketsEntity.setBuckets(buckets);
 
-            final BucketsEntity bucketsEntity = new BucketsEntity();
+        return generateOkResponse(bucketsEntity).build();
+    }
 
-            if (userBuckets != null) {
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("registries/{registry-id}/buckets/{bucket-id}/flows")
+    @ApiOperation(value = "Gets the flows from the specified registry and bucket for the current user", response = BucketsEntity.class, authorizations = {
+            @Authorization(value = "Read - /flow")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+            @ApiResponse(code = 401, message = "Client could not be authenticated."),
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 404, message = "The specified resource could not be found."),
+            @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getFlows(
+            @ApiParam(
+                value = "The registry id.",
+                required = true
+            )
+            @PathParam("registry-id") String registryId,
+            @ApiParam(
+                    value = "The bucket id.",
+                    required = true
+            )
+            @PathParam("bucket-id") String bucketId) {
 
-                final Set<BucketEntity> bucketSet = new HashSet<>();
-                for (final Bucket userBucket : userBuckets) {
-                    final BucketDTO bucket = new BucketDTO();
-                    bucket.setId(userBucket.getIdentifier());
-                    bucket.setName(userBucket.getName());
-                    bucket.setDescription(userBucket.getDescription());
-                    bucket.setCreated(userBucket.getCreatedTimestamp());
+        authorizeFlow();
 
-                    final BucketEntity bucketEntity = new BucketEntity();
-                    bucketEntity.setBucket(bucket);
+        final Set<VersionedFlowEntity> versionedFlows = serviceFacade.getFlowsForUser(registryId, bucketId, NiFiUserUtils.getNiFiUser());
 
-                    bucketSet.add(bucketEntity);
-                }
+        final VersionedFlowsEntity versionedFlowsEntity = new VersionedFlowsEntity();
+        versionedFlowsEntity.setVersionedFlows(versionedFlows);
 
-                bucketsEntity.setBuckets(bucketSet);
-            }
+        return generateOkResponse(versionedFlowsEntity).build();
+    }
 
-            return generateOkResponse(bucketsEntity).build();
-        } catch (final IOException ioe) {
-            throw new NiFiCoreException("Unable to obtain bucket listing: " + ioe.getMessage(), ioe);
-        }
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("registries/{registry-id}/buckets/{bucket-id}/flows/{flow-id}/versions")
+    @ApiOperation(value = "Gets the flow versions from the specified registry and bucket for the specified flow for the current user", response = BucketsEntity.class, authorizations = {
+            @Authorization(value = "Read - /flow")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+            @ApiResponse(code = 401, message = "Client could not be authenticated."),
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 404, message = "The specified resource could not be found."),
+            @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getVersions(
+            @ApiParam(
+                    value = "The registry id.",
+                    required = true
+            )
+            @PathParam("registry-id") String registryId,
+            @ApiParam(
+                    value = "The bucket id.",
+                    required = true
+            )
+            @PathParam("bucket-id") String bucketId,
+            @ApiParam(
+                    value = "The flow id.",
+                    required = true
+            )
+            @PathParam("flow-id") String flowId) {
+
+        authorizeFlow();
+
+        final Set<VersionedFlowSnapshotMetadataEntity> versionedFlowSnapshotMetadataSet = serviceFacade.getFlowVersionsForUser(registryId, bucketId, flowId, NiFiUserUtils.getNiFiUser());
+
+        final VersionedFlowSnapshotMetadataSetEntity versionedFlowSnapshotMetadataSetEntity = new VersionedFlowSnapshotMetadataSetEntity();
+        versionedFlowSnapshotMetadataSetEntity.setVersionedFlowSnapshotMetadataSet(versionedFlowSnapshotMetadataSet);
+
+        return generateOkResponse(versionedFlowSnapshotMetadataSetEntity).build();
     }
 
     // --------------
@@ -2629,10 +2669,6 @@ public class FlowResource extends ApplicationResource {
         this.processGroupResource = processGroupResource;
     }
 
-    public void setControllerResource(ControllerResource controllerResource) {
-        this.controllerResource = controllerResource;
-    }
-
     public void setControllerServiceResource(ControllerServiceResource controllerServiceResource) {
         this.controllerServiceResource = controllerServiceResource;
     }
@@ -2644,8 +2680,4 @@ public class FlowResource extends ApplicationResource {
     public void setAuthorizer(Authorizer authorizer) {
         this.authorizer = authorizer;
     }
-
-    public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
-        this.flowRegistryClient = flowRegistryClient;
-    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 27216a4..b010bf3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,39 +17,12 @@
 
 package org.apache.nifi.web.api;
 
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.Authorizer;
@@ -58,6 +31,7 @@ import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.authorization.user.NiFiUserUtils;
 import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.registry.flow.ComponentType;
@@ -83,7 +57,7 @@ import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
-import org.apache.nifi.web.api.entity.VersionedFlowEntity;
+import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowUpdateRequestEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
@@ -96,12 +70,37 @@ import org.apache.nifi.web.util.Pause;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 @Path("/versions")
 @Api(value = "/versions", description = "Endpoint for managing version control for a flow")
@@ -368,7 +367,7 @@ public class VersionsResource extends ApplicationResource {
     })
     public Response startVersionControl(
         @ApiParam("The process group id.") @PathParam("id") final String groupId,
-        @ApiParam(value = "The versioned flow details.", required = true) final VersionedFlowEntity requestEntity) throws IOException {
+        @ApiParam(value = "The versioned flow details.", required = true) final StartVersionControlRequestEntity requestEntity) throws IOException {
 
         // Verify the request
         final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
@@ -390,6 +389,12 @@ public class VersionsResource extends ApplicationResource {
             throw new IllegalArgumentException("The Registry ID must be supplied.");
         }
 
+        // ensure we're not attempting to version the root group
+        final ProcessGroupEntity root = serviceFacade.getProcessGroup(FlowController.ROOT_GROUP_ID_ALIAS);
+        if (root.getId().equals(groupId)) {
+            throw new IllegalArgumentException("The Root Process Group cannot be versioned.");
+        }
+
         if (isReplicateRequest()) {
             // We first have to obtain a "lock" on all nodes in the cluster so that multiple Version Control requests
             // are not being made simultaneously. We do this by making a POST to /nifi-api/versions/start-requests.
@@ -688,6 +693,7 @@ public class VersionsResource extends ApplicationResource {
                 versionControlInfoDto.setBucketId(snapshotMetadata.getBucketIdentifier());
                 versionControlInfoDto.setCurrent(true);
                 versionControlInfoDto.setFlowId(snapshotMetadata.getFlowIdentifier());
+                versionControlInfoDto.setFlowName(snapshotMetadata.getFlowName());
                 versionControlInfoDto.setGroupId(groupId);
                 versionControlInfoDto.setModified(false);
                 versionControlInfoDto.setVersion(snapshotMetadata.getVersion());
@@ -1139,12 +1145,13 @@ public class VersionsResource extends ApplicationResource {
                     updateRequestDto.setLastUpdated(new Date());
                     updateRequestDto.setProcessGroupId(groupId);
                     updateRequestDto.setRequestId(requestId);
-                    updateRequestDto.setUri(generateResourceUri("versions", "update-requests", requestId));
+                    updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
 
                     final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
                     updateRequestEntity.setProcessGroupRevision(revisionDto);
                     updateRequestEntity.setRequest(updateRequestDto);
 
+                    request.markComplete(currentVersionEntity);
                     return generateOkResponse(updateRequestEntity).build();
                 }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 8e0f0c7..3639b18 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,7 +16,6 @@
  */
 package org.apache.nifi.web.api.dto;
 
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -245,7 +244,6 @@ public final class DtoFactory {
         dto.setSupportsManagedAuthorizer(AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer));
         dto.setSupportsConfigurableUsersAndGroups(AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer));
         dto.setSupportsConfigurableAuthorizer(AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer));
-        dto.setSupportsFlowVersioning(CollectionUtils.isNotEmpty(flowRegistryClient.getRegistryIdentifiers()));
 
         final Date now = new Date();
         dto.setTimeOffset(TimeZone.getDefault().getOffset(now.getTime()));
@@ -2191,6 +2189,8 @@ public final class DtoFactory {
         dto.setRegistryId(versionControlInfo.getRegistryIdentifier());
         dto.setBucketId(versionControlInfo.getBucketIdentifier());
         dto.setFlowId(versionControlInfo.getFlowIdentifier());
+        // TODO - need to get flow name here
+        dto.setFlowName(group.getName());
         dto.setVersion(versionControlInfo.getVersion());
         dto.setCurrent(versionControlInfo.getCurrent().orElse(null));
         dto.setModified(versionControlInfo.getModified().orElse(null));
@@ -3409,6 +3409,7 @@ public final class DtoFactory {
         copy.setRegistryId(original.getRegistryId());
         copy.setBucketId(original.getBucketId());
         copy.setFlowId(original.getFlowId());
+        copy.setFlowName(original.getFlowName());
         copy.setVersion(original.getVersion());
         copy.setCurrent(original.getCurrent());
         copy.setModified(original.getModified());

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index 650d4b3..7cf61ea 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -159,6 +159,13 @@ public interface ProcessGroupDAO {
     void verifyDelete(String groupId);
 
     /**
+     * Verifies the specified registry can be removed.
+     *
+     * @param registryId registry id
+     */
+    void verifyDeleteFlowRegistry(String registryId);
+
+    /**
      * Deletes the specified process group.
      *
      * @param groupId The process group id

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
index 83b5c6d..8c22ff4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
@@ -17,11 +17,15 @@
 
 package org.apache.nifi.web.dao;
 
-import java.util.Set;
-
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.web.api.dto.RegistryDTO;
 
+import java.util.Set;
+
 public interface RegistryDAO {
 
     FlowRegistry createFlowRegistry(RegistryDTO registryDto);
@@ -30,6 +34,14 @@ public interface RegistryDAO {
 
     Set<FlowRegistry> getFlowRegistries();
 
+    Set<FlowRegistry> getFlowRegistriesForUser(NiFiUser user);
+
+    Set<Bucket> getBucketsForUser(String registry, NiFiUser user);
+
+    Set<VersionedFlow> getFlowsForUser(String registryId, String bucketId, NiFiUser user);
+
+    Set<VersionedFlowSnapshotMetadata> getFlowVersionsForUser(String registryId, String bucketId, String flowId, NiFiUser user);
+
     FlowRegistry removeFlowRegistry(String registryId);
 
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
index eb2ac76..19f2de4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
@@ -17,15 +17,21 @@
 
 package org.apache.nifi.web.dao.impl;
 
-import java.util.Set;
-import java.util.stream.Collectors;
-
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.web.NiFiCoreException;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.api.dto.RegistryDTO;
 import org.apache.nifi.web.dao.RegistryDAO;
 
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 public class FlowRegistryDAO implements RegistryDAO {
     private FlowRegistryClient flowRegistryClient;
 
@@ -52,6 +58,53 @@ public class FlowRegistryDAO implements RegistryDAO {
     }
 
     @Override
+    public Set<FlowRegistry> getFlowRegistriesForUser(final NiFiUser user) {
+        // TODO - implement to be user specific
+        return getFlowRegistries();
+    }
+
+    @Override
+    public Set<Bucket> getBucketsForUser(final String registryId, final NiFiUser user) {
+        try {
+            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
+            if (flowRegistry == null) {
+                throw new IllegalArgumentException("The specified registry id is unknown to this NiFi.");
+            }
+
+            return flowRegistry.getBuckets(user);
+        } catch (final IOException ioe) {
+            throw new NiFiCoreException("Unable to obtain bucket listing: " + ioe.getMessage(), ioe);
+        }
+    }
+
+
+    @Override
+    public Set<VersionedFlow> getFlowsForUser(String registryId, String bucketId, NiFiUser user) {
+        final Set<Bucket> bucketsForUser = getBucketsForUser(registryId, user);
+
+        // TODO - implement getBucket(bucketId, user)
+        final Bucket bucket = bucketsForUser.stream().filter(b -> b.getIdentifier().equals(bucketId)).findFirst().orElse(null);
+        if (bucket == null) {
+            throw new IllegalArgumentException("The specified bucket is not available.");
+        }
+
+        return bucket.getVersionedFlows();
+    }
+
+    @Override
+    public Set<VersionedFlowSnapshotMetadata> getFlowVersionsForUser(String registryId, String bucketId, String flowId, NiFiUser user) {
+        final Set<VersionedFlow> flowsForUser = getFlowsForUser(registryId, bucketId, user);
+
+        // TODO - implement getFlow(bucketId, flowId, user)
+        final VersionedFlow versionedFlow = flowsForUser.stream().filter(vf -> vf.getIdentifier().equals(flowId)).findFirst().orElse(null);
+        if (versionedFlow == null) {
+            throw new IllegalArgumentException("The specified flow is not available.");
+        }
+
+        return versionedFlow.getSnapshotMetadata();
+    }
+
+    @Override
     public FlowRegistry removeFlowRegistry(final String registryId) {
         final FlowRegistry registry = flowRegistryClient.removeFlowRegistry(registryId);
         if (registry == null) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 7828337..963220e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -39,9 +39,11 @@ import org.apache.nifi.web.dao.ProcessGroupDAO;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
+import java.util.stream.Collectors;
 
 public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGroupDAO {
 
@@ -295,6 +297,26 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
+    public void verifyDeleteFlowRegistry(String registryId) {
+        final ProcessGroup rootGroup = flowController.getRootGroup();
+
+        final VersionControlInformation versionControlInformation = rootGroup.getVersionControlInformation();
+        if (versionControlInformation != null && versionControlInformation.getRegistryIdentifier().equals(registryId)) {
+            throw new IllegalStateException("The Registry cannot be removed because a Process Group currently under version control is tracking to it.");
+        }
+
+        final Set<VersionControlInformation> trackedVersionControlInformation = rootGroup.findAllProcessGroups().stream()
+                .map(group -> group.getVersionControlInformation())
+                .filter(Objects::nonNull)
+                .filter(vci -> vci.getRegistryIdentifier().equals(registryId))
+                .collect(Collectors.toSet());
+
+        if (!trackedVersionControlInformation.isEmpty()) {
+            throw new IllegalStateException("The Registry cannot be removed because a Process Group currently under version control is tracking to it.");
+        }
+    }
+
+    @Override
     public void deleteProcessGroup(String processGroupId) {
         // get the group
         ProcessGroup group = locateProcessGroup(flowController, processGroupId);

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index e71de67..43843ef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -213,7 +213,6 @@
         <property name="remoteProcessGroupResource" ref="remoteProcessGroupResource"/>
         <property name="connectionResource" ref="connectionResource"/>
         <property name="templateResource" ref="templateResource"/>
-        <property name="controllerResource" ref="controllerResource"/>
         <property name="controllerServiceResource" ref="controllerServiceResource"/>
         <property name="reportingTaskResource" ref="reportingTaskResource"/>
         <property name="processGroupResource" ref="processGroupResource"/>
@@ -221,7 +220,6 @@
         <property name="clusterCoordinator" ref="clusterCoordinator"/>
         <property name="requestReplicator" ref="requestReplicator" />
         <property name="flowController" ref="flowController" />
-        <property name="flowRegistryClient" ref="flowRegistryClient" />
     </bean>
     <bean id="resourceResource" class="org.apache.nifi.web.api.ResourceResource" scope="singleton">
         <property name="serviceFacade" ref="serviceFacade"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
index c57b76f..258bdad 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
@@ -116,6 +116,7 @@
         <jsp:include page="/WEB-INF/partials/canvas/fill-color-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/connections-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/save-flow-version-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/import-flow-version-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/registry-configuration-dialog.jsp"/>
         <div id="canvas-container" class="unselectable"></div>
         <div id="canvas-tooltips">

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
new file mode 100644
index 0000000..5169c7c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
@@ -0,0 +1,44 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="import-flow-version-dialog" layout="column" class="hidden large-dialog">
+    <div class="dialog-content">
+        <div class="setting">
+            <div class="setting-name">Registry</div>
+            <div class="setting-field">
+                <div id="import-flow-version-registry-combo"></div>
+                <div id="import-flow-version-registry" class="hidden"></div>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Location</div>
+            <div class="setting-field">
+                <div id="import-flow-version-bucket-combo"></div>
+                <div id="import-flow-version-bucket" class="hidden"></div>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Name</div>
+            <div class="setting-field">
+                <span id="import-flow-version-process-group-id" class="hidden"></span>
+                <div id="import-flow-version-name-combo"></div>
+                <div id="import-flow-version-name" class="hidden"></div>
+            </div>
+        </div>
+        <div id="import-flow-version-table"></div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
index 0732b3d..86f4ba3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
@@ -128,13 +128,13 @@
                     <div class="button-spacer-large">&nbsp;</div>
                     <div id="operate-template" class="action-button" title="Create Template">
                         <button ng-click="appCtrl.nf.Actions['template'](appCtrl.nf.CanvasUtils.getSelection());"
-                                ng-disabled="!(appCtrl.nf.CanvasUtils.canWrite() && (appCtrl.nf.CanvasUtils.getSelection().empty() || appCtrl.nf.CanvasUtils.canRead(appCtrl.nf.CanvasUtils.getSelection())));">
+                                ng-disabled="!(appCtrl.nf.CanvasUtils.canWriteCurrentGroup() && (appCtrl.nf.CanvasUtils.getSelection().empty() || appCtrl.nf.CanvasUtils.canRead(appCtrl.nf.CanvasUtils.getSelection())));">
                             <div class="graph-control-action-icon icon icon-template-save"></div></button>
                     </div>
                     <div class="button-spacer-small">&nbsp;</div>
                     <div id="operate-template-upload" class="action-button" title="Upload Template">
                         <button ng-click="appCtrl.nf.Actions['uploadTemplate']();"
-                                ng-disabled="!(appCtrl.nf.CanvasUtils.canWrite() && appCtrl.nf.CanvasUtils.getSelection().empty());">
+                                ng-disabled="!(appCtrl.nf.CanvasUtils.canWriteCurrentGroup() && appCtrl.nf.CanvasUtils.getSelection().empty());">
                             <div class="graph-control-action-icon icon icon-template-import"></div></button>
                     </div>
                     <div class="clear"></div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/696d583b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
index cb538c6..3f1b6a0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/new-process-group-dialog.jsp
@@ -23,5 +23,8 @@
                 <input id="new-process-group-name" type="text"/>
             </div>
         </div>
+        <div class="setting">
+            <span id="import-process-group-link" class="link"><i class="fa fa-cloud-download" aria-hidden="true" style="margin-left: 5px; margin-right: 5px;"></i>Import version...</span>
+        </div>
     </div>
 </div>
\ No newline at end of file


[24/50] nifi git commit: NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated autho

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
index da5880c..9b3ba94 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -37,6 +38,7 @@ import java.util.UUID;
 
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 
 import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.core.JsonGenerator;
@@ -150,7 +152,7 @@ public class FileBasedFlowRegistry implements FlowRegistry {
                     try {
                         final VersionedFlow versionedFlow = getVersionedFlow(bucketIdentifier, flowIdentifier);
                         versionedFlows.add(versionedFlow);
-                    } catch (UnknownResourceException e) {
+                    } catch (NiFiRegistryException e) {
                         continue;
                     }
                 }
@@ -164,9 +166,38 @@ public class FileBasedFlowRegistry implements FlowRegistry {
         return buckets;
     }
 
+    @Override
+    public Bucket getBucket(String bucketId) throws IOException, NiFiRegistryException {
+        return getBucket(bucketId, null);
+    }
+
+    @Override
+    public Bucket getBucket(String bucketId, NiFiUser user) throws IOException, NiFiRegistryException {
+        return getBuckets(user).stream().filter(b -> b.getIdentifier().equals(bucketId)).findFirst().orElse(null);
+    }
+
+    @Override
+    public Set<VersionedFlow> getFlows(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final Bucket bucket = getBuckets(user).stream().filter(b -> bucketId.equals(b.getIdentifier())).findFirst().orElse(null);
+        if (bucket == null) {
+            return Collections.emptySet();
+        }
+
+        return bucket.getVersionedFlows();
+    }
+
+    @Override
+    public Set<VersionedFlowSnapshotMetadata> getFlowVersions(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final VersionedFlow flow = getFlows(bucketId, user).stream().filter(f -> flowId.equals(f.getIdentifier())).findFirst().orElse(null);
+        if (flow == null) {
+            return Collections.emptySet();
+        }
+
+        return flow.getSnapshotMetadata();
+    }
 
     @Override
-    public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, UnknownResourceException {
+    public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, NiFiRegistryException {
         Objects.requireNonNull(flow);
         Objects.requireNonNull(flow.getBucketIdentifier());
         Objects.requireNonNull(flow.getName());
@@ -174,7 +205,7 @@ public class FileBasedFlowRegistry implements FlowRegistry {
         // Verify that bucket exists
         final File bucketDir = new File(directory, flow.getBucketIdentifier());
         if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
+            throw new NiFiRegistryException("No bucket exists with ID " + flow.getBucketIdentifier());
         }
 
         // Verify that there is no flow with the same name in that bucket
@@ -213,8 +244,8 @@ public class FileBasedFlowRegistry implements FlowRegistry {
     }
 
     @Override
-    public synchronized VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments)
-            throws IOException, UnknownResourceException {
+    public synchronized VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments, final int expectedVersion)
+        throws IOException, NiFiRegistryException {
         Objects.requireNonNull(flow);
         Objects.requireNonNull(flow.getBucketIdentifier());
         Objects.requireNonNull(flow.getName());
@@ -223,13 +254,13 @@ public class FileBasedFlowRegistry implements FlowRegistry {
         // Verify that the bucket exists
         final File bucketDir = new File(directory, flow.getBucketIdentifier());
         if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
+            throw new NiFiRegistryException("No bucket exists with ID " + flow.getBucketIdentifier());
         }
 
         // Verify that the flow exists
         final File flowDir = new File(bucketDir, flow.getIdentifier());
         if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flow.getIdentifier() + " exists for Bucket with ID " + flow.getBucketIdentifier());
+            throw new NiFiRegistryException("No Flow with ID " + flow.getIdentifier() + " exists for Bucket with ID " + flow.getBucketIdentifier());
         }
 
         final File[] versionDirs = flowDir.listFiles();
@@ -291,17 +322,17 @@ public class FileBasedFlowRegistry implements FlowRegistry {
     }
 
     @Override
-    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
         // Verify that the bucket exists
         final File bucketDir = new File(directory, bucketId);
         if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+            throw new NiFiRegistryException("No bucket exists with ID " + bucketId);
         }
 
         // Verify that the flow exists
         final File flowDir = new File(bucketDir, flowId);
         if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + bucketId);
+            throw new NiFiRegistryException("No Flow with ID " + flowId + " exists for Bucket with ID " + bucketId);
         }
 
         final File[] versionDirs = flowDir.listFiles();
@@ -329,22 +360,22 @@ public class FileBasedFlowRegistry implements FlowRegistry {
     }
 
     @Override
-    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, int version) throws IOException, UnknownResourceException {
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, int version) throws IOException, NiFiRegistryException {
         // Verify that the bucket exists
         final File bucketDir = new File(directory, bucketId);
         if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+            throw new NiFiRegistryException("No bucket exists with ID " + bucketId);
         }
 
         // Verify that the flow exists
         final File flowDir = new File(bucketDir, flowId);
         if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
+            throw new NiFiRegistryException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
         }
 
         final File versionDir = new File(flowDir, String.valueOf(version));
         if (!versionDir.exists()) {
-            throw new UnknownResourceException("Flow with ID " + flowId + " in Bucket with ID " + bucketId + " does not contain a snapshot with version " + version);
+            throw new NiFiRegistryException("Flow with ID " + flowId + " in Bucket with ID " + bucketId + " does not contain a snapshot with version " + version);
         }
 
         final File contentsFile = new File(versionDir, "flow.xml");
@@ -383,17 +414,17 @@ public class FileBasedFlowRegistry implements FlowRegistry {
     }
 
     @Override
-    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
         // Verify that the bucket exists
         final File bucketDir = new File(directory, bucketId);
         if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+            throw new NiFiRegistryException("No bucket exists with ID " + bucketId);
         }
 
         // Verify that the flow exists
         final File flowDir = new File(bucketDir, flowId);
         if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
+            throw new NiFiRegistryException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
         }
 
         final File flowPropsFile = new File(flowDir, "flow.properties");

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
new file mode 100644
index 0000000..26be69b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
@@ -0,0 +1,235 @@
+/*
+ * 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.registry.flow;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.FlowClient;
+import org.apache.nifi.registry.client.FlowSnapshotClient;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+
+public class RestBasedFlowRegistry implements FlowRegistry {
+
+    private final FlowRegistryClient flowRegistryClient;
+    private final String identifier;
+    private final SSLContext sslContext;
+    private volatile String description;
+    private volatile String url;
+    private volatile String name;
+
+    private NiFiRegistryClient registryClient;
+
+    public RestBasedFlowRegistry(final FlowRegistryClient flowRegistryClient, final String identifier, final String url, final SSLContext sslContext, final String name) {
+        this.flowRegistryClient = flowRegistryClient;
+        this.identifier = identifier;
+        this.url = url;
+        this.name = name;
+        this.sslContext = sslContext;
+    }
+
+    private synchronized NiFiRegistryClient getRegistryClient() {
+        if (registryClient != null) {
+            return registryClient;
+        }
+
+        final NiFiRegistryClientConfig config = new NiFiRegistryClientConfig.Builder()
+            .connectTimeout(30000)
+            .readTimeout(30000)
+            .sslContext(sslContext)
+            .baseUrl(url)
+            .build();
+
+        registryClient = new JerseyNiFiRegistryClient.Builder()
+            .config(config)
+            .build();
+
+        return registryClient;
+    }
+
+    private synchronized void invalidateClient() {
+        this.registryClient = null;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    @Override
+    public String getURL() {
+        return url;
+    }
+
+    @Override
+    public synchronized void setURL(final String url) {
+        this.url = url;
+        invalidateClient();
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Set<Bucket> getBuckets(final NiFiUser user) throws IOException, NiFiRegistryException {
+        final BucketClient bucketClient = getRegistryClient().getBucketClient(user.isAnonymous() ? null : user.getIdentity());
+        return new HashSet<>(bucketClient.getAll());
+    }
+
+    @Override
+    public Bucket getBucket(final String bucketId) throws IOException, NiFiRegistryException {
+        final BucketClient bucketClient = getRegistryClient().getBucketClient();
+        return bucketClient.get(bucketId);
+    }
+
+    @Override
+    public Bucket getBucket(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final BucketClient bucketClient = getRegistryClient().getBucketClient(user.isAnonymous() ? null : user.getIdentity());
+        return bucketClient.get(bucketId);
+    }
+
+
+    @Override
+    public Set<VersionedFlow> getFlows(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final FlowClient flowClient = getRegistryClient().getFlowClient(user.isAnonymous() ? null : user.getIdentity());
+        return new HashSet<>(flowClient.getByBucket(bucketId));
+    }
+
+    @Override
+    public Set<VersionedFlowSnapshotMetadata> getFlowVersions(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(user.isAnonymous() ? null : user.getIdentity());
+        return new HashSet<>(snapshotClient.getSnapshotMetadata(bucketId, flowId));
+    }
+
+    @Override
+    public VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, NiFiRegistryException {
+        final FlowClient flowClient = getRegistryClient().getFlowClient();
+        return flowClient.create(flow);
+    }
+
+    @Override
+    public VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments, final int expectedVersion)
+            throws IOException, NiFiRegistryException {
+
+        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient();
+        final VersionedFlowSnapshot versionedFlowSnapshot = new VersionedFlowSnapshot();
+        versionedFlowSnapshot.setFlowContents(snapshot);
+
+        final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
+        metadata.setBucketIdentifier(flow.getBucketIdentifier());
+        metadata.setFlowIdentifier(flow.getIdentifier());
+        metadata.setFlowName(flow.getName());
+        metadata.setTimestamp(System.currentTimeMillis());
+        metadata.setVersion(expectedVersion);
+        metadata.setComments(comments);
+
+        versionedFlowSnapshot.setSnapshotMetadata(metadata);
+        return snapshotClient.create(versionedFlowSnapshot);
+    }
+
+    @Override
+    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
+        return (int) getRegistryClient().getFlowClient().get(bucketId, flowId).getVersionCount();
+    }
+
+    @Override
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version) throws IOException, NiFiRegistryException {
+        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient();
+        final VersionedFlowSnapshot flowSnapshot = snapshotClient.get(bucketId, flowId, version);
+
+        final VersionedProcessGroup contents = flowSnapshot.getFlowContents();
+        for (final VersionedProcessGroup child : contents.getProcessGroups()) {
+            populateVersionedContentsRecursively(child);
+        }
+
+        return flowSnapshot;
+    }
+
+    private void populateVersionedContentsRecursively(final VersionedProcessGroup group) throws NiFiRegistryException, IOException {
+        if (group == null) {
+            return;
+        }
+
+        final VersionedFlowCoordinates coordinates = group.getVersionedFlowCoordinates();
+        if (coordinates != null) {
+            final String registryUrl = coordinates.getRegistryUrl();
+            final String bucketId = coordinates.getBucketId();
+            final String flowId = coordinates.getFlowId();
+            final int version = coordinates.getVersion();
+
+            final String registryId = flowRegistryClient.getFlowRegistryId(registryUrl);
+            if (registryId == null) {
+                throw new NiFiRegistryException("Flow contains a reference to another Versioned Flow located at URL " + registryUrl
+                    + " but NiFi is not configured to communicate with a Flow Registry at that URL");
+            }
+
+            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
+            final VersionedFlowSnapshot snapshot = flowRegistry.getFlowContents(bucketId, flowId, version);
+            final VersionedProcessGroup contents = snapshot.getFlowContents();
+
+            group.setComments(contents.getComments());
+            group.setConnections(contents.getConnections());
+            group.setControllerServices(contents.getControllerServices());
+            group.setFunnels(contents.getFunnels());
+            group.setInputPorts(contents.getInputPorts());
+            group.setLabels(contents.getLabels());
+            group.setOutputPorts(contents.getOutputPorts());
+            group.setProcessGroups(contents.getProcessGroups());
+            group.setProcessors(contents.getProcessors());
+            group.setRemoteProcessGroups(contents.getRemoteProcessGroups());
+            group.setVariables(contents.getVariables());
+        }
+
+        for (final VersionedProcessGroup child : group.getProcessGroups()) {
+            populateVersionedContentsRecursively(child);
+        }
+    }
+
+    @Override
+    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
+        final FlowClient flowClient = getRegistryClient().getFlowClient();
+        return flowClient.get(bucketId, flowId);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
index 828b970..d5d0d86 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
@@ -23,7 +23,13 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
+import javax.net.ssl.SSLContext;
+
+import org.apache.nifi.framework.security.util.SslContextFactory;
+import org.apache.nifi.util.NiFiProperties;
+
 public class StandardFlowRegistryClient implements FlowRegistryClient {
+    private NiFiProperties nifiProperties;
     private ConcurrentMap<String, FlowRegistry> registryById = new ConcurrentHashMap<>();
 
     @Override
@@ -59,6 +65,16 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
 
             registry.setName(registryName);
             registry.setDescription(description);
+        } else if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
+            final SSLContext sslContext = SslContextFactory.createSslContext(nifiProperties, false);
+            if (sslContext == null && uriScheme.equalsIgnoreCase("https")) {
+                throw new RuntimeException("Failed to create Flow Registry for URI " + registryUrl
+                    + " because this NiFi is not configured with a Keystore/Truststore, so it is not capable of communicating with a secure Registry. "
+                    + "Please populate NiFi's Keystore/Truststore properties or connect to a NiFi Registry over http instead of https.");
+            }
+
+            registry = new RestBasedFlowRegistry(this, registryId, registryUrl, sslContext, registryName);
+            registry.setDescription(description);
         } else {
             throw new IllegalArgumentException("Cannot create Flow Registry with URI of " + registryUrl
                 + " because there are no known implementations of Flow Registries that can handle URIs of scheme " + uriScheme);
@@ -72,4 +88,8 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
     public FlowRegistry removeFlowRegistry(final String registryId) {
         return registryById.remove(registryId);
     }
+
+    public void setProperties(final NiFiProperties nifiProperties) {
+        this.nifiProperties = nifiProperties;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
index 41b98ed..aaba126 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
@@ -17,21 +17,131 @@
 
 package org.apache.nifi.registry.flow;
 
+import java.util.Objects;
 import java.util.Optional;
 
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+
 public class StandardVersionControlInformation implements VersionControlInformation {
 
     private final String registryIdentifier;
+    private volatile String registryName;
     private final String bucketIdentifier;
+    private volatile String bucketName;
     private final String flowIdentifier;
+    private volatile String flowName;
+    private volatile String flowDescription;
     private final int version;
     private volatile VersionedProcessGroup flowSnapshot;
     private volatile Boolean modified = null;
     private volatile Boolean current = null;
 
-    public StandardVersionControlInformation(final String registryId, final String bucketId, final String flowId, final int version,
+    public static class Builder {
+        private String registryIdentifier;
+        private String registryName;
+        private String bucketIdentifier;
+        private String bucketName;
+        private String flowIdentifier;
+        private String flowName;
+        private String flowDescription;
+        private int version;
+        private VersionedProcessGroup flowSnapshot;
+        private Boolean modified = null;
+        private Boolean current = null;
+
+        public Builder registryId(String registryId) {
+            this.registryIdentifier = registryId;
+            return this;
+        }
+
+        public Builder registryName(String registryName) {
+            this.registryName = registryName;
+            return this;
+        }
+
+        public Builder bucketId(String bucketId) {
+            this.bucketIdentifier = bucketId;
+            return this;
+        }
+
+        public Builder bucketName(String bucketName) {
+            this.bucketName = bucketName;
+            return this;
+        }
+
+        public Builder flowId(String flowId) {
+            this.flowIdentifier = flowId;
+            return this;
+        }
+
+        public Builder flowName(String flowName) {
+            this.flowName = flowName;
+            return this;
+        }
+
+        public Builder flowDescription(String flowDescription) {
+            this.flowDescription = flowDescription;
+            return this;
+        }
+
+        public Builder version(int version) {
+            this.version = version;
+            return this;
+        }
+
+        public Builder modified(Boolean modified) {
+            this.modified = modified;
+            return this;
+        }
+
+        public Builder current(Boolean current) {
+            this.current = current;
+            return this;
+        }
+
+        public Builder flowSnapshot(VersionedProcessGroup snapshot) {
+            this.flowSnapshot = snapshot;
+            return this;
+        }
+
+        public static Builder fromDto(VersionControlInformationDTO dto) {
+            Builder builder = new Builder();
+            builder.registryId(dto.getRegistryId())
+                .registryName(dto.getRegistryName())
+                .bucketId(dto.getBucketId())
+                .bucketName(dto.getBucketName())
+                .flowId(dto.getFlowId())
+                .flowName(dto.getFlowName())
+                .flowDescription(dto.getFlowDescription())
+                .current(dto.getCurrent())
+                .modified(dto.getModified())
+                .version(dto.getVersion());
+
+            return builder;
+        }
+
+        public StandardVersionControlInformation build() {
+            Objects.requireNonNull(registryIdentifier, "Registry ID must be specified");
+            Objects.requireNonNull(bucketIdentifier, "Bucket ID must be specified");
+            Objects.requireNonNull(flowIdentifier, "Flow ID must be specified");
+            Objects.requireNonNull(version, "Version must be specified");
+
+            final StandardVersionControlInformation svci = new StandardVersionControlInformation(registryIdentifier, registryName,
+                bucketIdentifier, flowIdentifier, version, flowSnapshot, modified, current);
+
+            svci.setBucketName(bucketName);
+            svci.setFlowName(flowName);
+            svci.setFlowDescription(flowDescription);
+
+            return svci;
+        }
+    }
+
+
+    public StandardVersionControlInformation(final String registryId, final String registryName, final String bucketId, final String flowId, final int version,
         final VersionedProcessGroup snapshot, final Boolean modified, final Boolean current) {
         this.registryIdentifier = registryId;
+        this.registryName = registryName;
         this.bucketIdentifier = bucketId;
         this.flowIdentifier = flowId;
         this.version = version;
@@ -40,21 +150,58 @@ public class StandardVersionControlInformation implements VersionControlInformat
         this.current = current;
     }
 
+
     @Override
     public String getRegistryIdentifier() {
         return registryIdentifier;
     }
 
     @Override
+    public String getRegistryName() {
+        return registryName;
+    }
+
+    public void setRegistryName(final String registryName) {
+        this.registryName = registryName;
+    }
+
+    @Override
     public String getBucketIdentifier() {
         return bucketIdentifier;
     }
 
     @Override
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public void setBucketName(final String bucketName) {
+        this.bucketName = bucketName;
+    }
+
+    @Override
     public String getFlowIdentifier() {
         return flowIdentifier;
     }
 
+    public void setFlowName(String flowName) {
+        this.flowName = flowName;
+    }
+
+    @Override
+    public String getFlowName() {
+        return flowName;
+    }
+
+    public void setFlowDescription(String flowDescription) {
+        this.flowDescription = flowDescription;
+    }
+
+    @Override
+    public String getFlowDescription() {
+        return flowDescription;
+    }
+
     @Override
     public int getVersion() {
         return version;

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
index a75d112..a10a1b8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
@@ -80,14 +80,11 @@ public class NiFiRegistryFlowMapper {
     // created before attempting to create the connection, where the ConnectableDTO is converted.
     private Map<String, String> versionedComponentIds = new HashMap<>();
 
-    public InstantiatedVersionedProcessGroup mapProcessGroup(final ProcessGroup group, final FlowRegistryClient registryClient) {
+    public InstantiatedVersionedProcessGroup mapProcessGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean mapDescendantVersionedFlows) {
         versionedComponentIds.clear();
-        final InstantiatedVersionedProcessGroup mapped = mapGroup(group, registryClient, true);
+        final InstantiatedVersionedProcessGroup mapped = mapGroup(group, registryClient, true, mapDescendantVersionedFlows);
 
-        // TODO: Test that this works properly
         populateReferencedAncestorServices(group, mapped);
-
-        // TODO: Test that this works properly
         populateReferencedAncestorVariables(group, mapped);
 
         return mapped;
@@ -149,7 +146,10 @@ public class NiFiRegistryFlowMapper {
         if (!implicitlyDefinedVariables.isEmpty()) {
             // Merge the implicit variables with the explicitly defined variables for the Process Group
             // and set those as the Versioned Group's variables.
-            implicitlyDefinedVariables.putAll(versionedGroup.getVariables());
+            if (versionedGroup.getVariables() != null) {
+                implicitlyDefinedVariables.putAll(versionedGroup.getVariables());
+            }
+
             versionedGroup.setVariables(implicitlyDefinedVariables);
         }
     }
@@ -167,7 +167,7 @@ public class NiFiRegistryFlowMapper {
     }
 
 
-    private InstantiatedVersionedProcessGroup mapGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean topLevel) {
+    private InstantiatedVersionedProcessGroup mapGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean topLevel, final boolean mapDescendantVersionedFlows) {
         final InstantiatedVersionedProcessGroup versionedGroup = new InstantiatedVersionedProcessGroup(group.getIdentifier(), group.getProcessGroupIdentifier());
         versionedGroup.setIdentifier(getId(group.getVersionedComponentId(), group.getIdentifier()));
         versionedGroup.setGroupIdentifier(getGroupId(group.getProcessGroupIdentifier()));
@@ -192,10 +192,22 @@ public class NiFiRegistryFlowMapper {
                 coordinates.setBucketId(versionControlInfo.getBucketIdentifier());
                 coordinates.setFlowId(versionControlInfo.getFlowIdentifier());
                 coordinates.setVersion(versionControlInfo.getVersion());
+                versionedGroup.setVersionedFlowCoordinates(coordinates);
+
+                // We need to register the Port ID -> Versioned Component ID's in our versionedComponentIds member variable for all input & output ports.
+                // Otherwise, we will not be able to lookup the port when connecting to it.
+                for (final Port port : group.getInputPorts()) {
+                    getId(port.getVersionedComponentId(), port.getIdentifier());
+                }
+                for (final Port port : group.getOutputPorts()) {
+                    getId(port.getVersionedComponentId(), port.getIdentifier());
+                }
 
                 // If the Process Group itself is remotely versioned, then we don't want to include its contents
                 // because the contents are remotely managed and not part of the versioning of this Process Group
-                return versionedGroup;
+                if (!mapDescendantVersionedFlows) {
+                    return versionedGroup;
+                }
             }
         }
 
@@ -228,7 +240,7 @@ public class NiFiRegistryFlowMapper {
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setProcessGroups(group.getProcessGroups().stream()
-            .map(grp -> mapGroup(grp, registryClient, false))
+            .map(grp -> mapGroup(grp, registryClient, false, mapDescendantVersionedFlows))
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setConnections(group.getConnections().stream()

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
index 8954f39..9e81d22 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
@@ -51,7 +51,7 @@
             <xs:element name="id" type="NonEmptyStringType" />
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="url" type="NonEmptyStringType" />
-            <xs:element name="description" type="NonEmptyStringType" />
+            <xs:element name="description" type="xs:string" />
         </xs:sequence>
     </xs:complexType>
     
@@ -180,7 +180,10 @@
         <xs:sequence>
             <xs:element name="registryId" type="NonEmptyStringType" />
             <xs:element name="bucketId" type="NonEmptyStringType" />
+            <xs:element name="bucketName" type="NonEmptyStringType" />
             <xs:element name="flowId" type="NonEmptyStringType" />
+            <xs:element name="flowName" type="NonEmptyStringType" />
+            <xs:element name="flowDescription" type="xs:string" />
             <xs:element name="version" type="NonEmptyStringType" />
         </xs:sequence>
     </xs:complexType>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
index fc42c62..d9f89aa 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
@@ -36,7 +36,9 @@
     </bean>
 
     <!-- flow registry -->
-    <bean id="flowRegistryClient" class="org.apache.nifi.registry.flow.StandardFlowRegistryClient" />
+    <bean id="flowRegistryClient" class="org.apache.nifi.registry.flow.StandardFlowRegistryClient">
+        <property name="properties" ref="nifiProperties" />
+    </bean>
 
     <!-- flow controller -->
     <bean id="flowController" class="org.apache.nifi.spring.FlowControllerFactoryBean">

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index e299059..be907ba 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -16,6 +16,14 @@
  */
 package org.apache.nifi.web;
 
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
 import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
@@ -23,7 +31,7 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.repository.claim.ContentDirection;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
-import org.apache.nifi.registry.flow.UnknownResourceException;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
@@ -86,6 +94,7 @@ import org.apache.nifi.web.api.entity.ControllerConfigurationEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceReferencingComponentsEntity;
 import org.apache.nifi.web.api.entity.CurrentUserEntity;
+import org.apache.nifi.web.api.entity.FlowComparisonEntity;
 import org.apache.nifi.web.api.entity.FlowConfigurationEntity;
 import org.apache.nifi.web.api.entity.FlowEntity;
 import org.apache.nifi.web.api.entity.FunnelEntity;
@@ -115,14 +124,6 @@ import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 
-import java.io.IOException;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-
 /**
  * Defines the NiFiServiceFacade interface.
  */
@@ -1274,6 +1275,17 @@ public interface NiFiServiceFacade {
     // ----------------------------------------
 
     /**
+     * Returns a FlowComparisonEntity that contains all of the local modifications since the Process Group
+     * was last synchronized with the Flow Registry
+     *
+     * @param processGroupId
+     * @return a FlowComparisonEntity that contains all of the local modifications since the Process Group
+     *         was last synchronized with the Flow Registry
+     * @throws IllegalStateException if the Process Group with the given ID is not under version control
+     */
+    FlowComparisonEntity getLocalModifications(String processGroupId) throws IOException, NiFiRegistryException;
+
+    /**
      * Returns the Version Control information for the Process Group with the given ID
      *
      * @param processGroupId the ID of the Process Group
@@ -1292,7 +1304,7 @@ public interface NiFiServiceFacade {
      *
      * @throws IOException if unable to communicate with the Flow Registry
      */
-    VersionedFlow registerVersionedFlow(String registryId, VersionedFlow flow) throws IOException, UnknownResourceException;
+    VersionedFlow registerVersionedFlow(String registryId, VersionedFlow flow) throws IOException, NiFiRegistryException;
 
     /**
      * Creates a snapshot of the Process Group with the given identifier, then creates a new Flow entity in the NiFi Registry
@@ -1312,11 +1324,13 @@ public interface NiFiServiceFacade {
      * @param flow the flow where the snapshot should be persisted
      * @param snapshot the Snapshot to persist
      * @param comments about the snapshot
+     * @param expectedVersion the version to save the flow as
      * @return the snapshot that represents what was stored in the registry
      *
      * @throws IOException if unable to communicate with the Flow Registry
      */
-    VersionedFlowSnapshot registerVersionedFlowSnapshot(String registryId, VersionedFlow flow, VersionedProcessGroup snapshot, String comments) throws IOException, UnknownResourceException;
+    VersionedFlowSnapshot registerVersionedFlowSnapshot(String registryId, VersionedFlow flow, VersionedProcessGroup snapshot, String comments, int expectedVersion)
+        throws IOException, NiFiRegistryException;
 
     /**
      * Updates the Version Control Information on the Process Group with the given ID
@@ -1351,6 +1365,15 @@ public interface NiFiServiceFacade {
     VersionedFlowSnapshot getVersionedFlowSnapshot(VersionControlInformationDTO versionControlInfo) throws IOException;
 
     /**
+     * Returns the name of the Flow Registry that is registered with the given ID. If no Flow Registry exists with the given ID, will return
+     * the ID itself as the name
+     *
+     * @param flowRegistryId the id of the flow registry
+     * @return the name of the Flow Registry that is registered with the given ID, or the ID itself if no Flow Registry is registered with the given ID
+     */
+    String getFlowRegistryName(String flowRegistryId);
+
+    /**
      * Determines which components currently exist in the Process Group with the given identifier and calculates which of those components
      * would be impacted by updating the Process Group to the provided snapshot
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index a319f27..89e00ba 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -90,7 +90,7 @@ import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.flow.UnknownResourceException;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedConnection;
@@ -101,11 +101,13 @@ import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.DifferenceType;
+import org.apache.nifi.registry.flow.diff.EvolvingDifferenceDescriptor;
 import org.apache.nifi.registry.flow.diff.FlowComparator;
 import org.apache.nifi.registry.flow.diff.FlowComparison;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
+import org.apache.nifi.registry.flow.diff.StaticDifferenceDescriptor;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
@@ -128,6 +130,7 @@ import org.apache.nifi.web.api.dto.BulletinDTO;
 import org.apache.nifi.web.api.dto.BulletinQueryDTO;
 import org.apache.nifi.web.api.dto.ClusterDTO;
 import org.apache.nifi.web.api.dto.ComponentDTO;
+import org.apache.nifi.web.api.dto.ComponentDifferenceDTO;
 import org.apache.nifi.web.api.dto.ComponentHistoryDTO;
 import org.apache.nifi.web.api.dto.ComponentReferenceDTO;
 import org.apache.nifi.web.api.dto.ComponentStateDTO;
@@ -205,6 +208,7 @@ import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceReferencingComponentEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceReferencingComponentsEntity;
 import org.apache.nifi.web.api.entity.CurrentUserEntity;
+import org.apache.nifi.web.api.entity.FlowComparisonEntity;
 import org.apache.nifi.web.api.entity.FlowConfigurationEntity;
 import org.apache.nifi.web.api.entity.FlowEntity;
 import org.apache.nifi.web.api.entity.FunnelEntity;
@@ -3628,6 +3632,10 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public VersionControlComponentMappingEntity registerFlowWithFlowRegistry(final String groupId, final StartVersionControlRequestEntity requestEntity) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final VersionControlInformation currentVci = processGroup.getVersionControlInformation();
+        final int expectedVersion = currentVci == null ? 1 : currentVci.getVersion() + 1;
+
         // Create a VersionedProcessGroup snapshot of the flow as it is currently.
         final InstantiatedVersionedProcessGroup versionedProcessGroup = createFlowSnapshot(groupId);
 
@@ -3660,8 +3668,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
             // add first snapshot to the flow in the registry
             final String comments = versionedFlow.getDescription() == null ? "Initial version of flow" : versionedFlow.getDescription();
-            registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, comments);
-        } catch (final UnknownResourceException e) {
+            registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, comments, expectedVersion);
+        } catch (final NiFiRegistryException e) {
             throw new IllegalArgumentException(e);
         } catch (final IOException ioe) {
             // will result in a 500: Internal Server Error
@@ -3671,12 +3679,15 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         // Update the Process Group with the new VersionControlInformation. (Send this to all nodes).
         final VersionControlInformationDTO vci = new VersionControlInformationDTO();
         vci.setBucketId(registeredFlow.getBucketIdentifier());
+        vci.setBucketName(registeredFlow.getBucketName());
         vci.setCurrent(true);
         vci.setFlowId(registeredFlow.getIdentifier());
         vci.setFlowName(registeredFlow.getName());
+        vci.setFlowDescription(registeredFlow.getDescription());
         vci.setGroupId(groupId);
         vci.setModified(false);
         vci.setRegistryId(registryId);
+        vci.setRegistryName(getFlowRegistryName(registryId));
         vci.setVersion(registeredSnapshot.getSnapshotMetadata().getVersion());
 
         final Map<String, String> mapping = dtoFactory.createVersionControlComponentMappingDto(versionedProcessGroup);
@@ -3707,12 +3718,67 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     private InstantiatedVersionedProcessGroup createFlowSnapshot(final String processGroupId) {
         final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId);
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient);
+        final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, false);
         return versionedGroup;
     }
 
     @Override
-    public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) throws IOException, UnknownResourceException {
+    public FlowComparisonEntity getLocalModifications(final String processGroupId) throws IOException, NiFiRegistryException {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId);
+        final VersionControlInformation versionControlInfo = processGroup.getVersionControlInformation();
+        if (versionControlInfo == null) {
+            throw new IllegalStateException("Process Group with ID " + processGroupId + " is not under Version Control");
+        }
+
+        final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(versionControlInfo.getRegistryIdentifier());
+        if (flowRegistry == null) {
+            throw new IllegalStateException("Process Group with ID " + processGroupId + " is tracking to a flow in Flow Registry with ID " + versionControlInfo.getRegistryIdentifier()
+                + " but cannot find a Flow Registry with that identifier");
+        }
+
+        final VersionedFlowSnapshot versionedFlowSnapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketIdentifier(),
+            versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion());
+
+
+        final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
+        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, true);
+        final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents();
+
+        final ComparableDataFlow localFlow = new ComparableDataFlow() {
+            @Override
+            public VersionedProcessGroup getContents() {
+                return localGroup;
+            }
+
+            @Override
+            public String getName() {
+                return "Local Flow";
+            }
+        };
+
+        final ComparableDataFlow registryFlow = new ComparableDataFlow() {
+            @Override
+            public VersionedProcessGroup getContents() {
+                return registryGroup;
+            }
+
+            @Override
+            public String getName() {
+                return "Versioned Flow";
+            }
+        };
+
+        final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, new EvolvingDifferenceDescriptor());
+        final FlowComparison flowComparison = flowComparator.compare();
+
+        final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison);
+        final FlowComparisonEntity entity = new FlowComparisonEntity();
+        entity.setComponentDifferences(differenceDtos);
+        return entity;
+    }
+
+    @Override
+    public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) throws IOException, NiFiRegistryException {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
         if (registry == null) {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
@@ -3721,7 +3787,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return registry.registerVersionedFlow(flow);
     }
 
-    private VersionedFlow getVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+    private VersionedFlow getVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
         if (registry == null) {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
@@ -3732,13 +3798,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public VersionedFlowSnapshot registerVersionedFlowSnapshot(final String registryId, final VersionedFlow flow,
-            final VersionedProcessGroup snapshot, final String comments) throws IOException, UnknownResourceException {
+        final VersionedProcessGroup snapshot, final String comments, final int expectedVersion) throws IOException, NiFiRegistryException {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
         if (registry == null) {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.registerVersionedFlowSnapshot(flow, snapshot, comments);
+        return registry.registerVersionedFlowSnapshot(flow, snapshot, comments, expectedVersion);
     }
 
     @Override
@@ -3778,12 +3844,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup localContents = mapper.mapProcessGroup(group, flowRegistryClient);
+        final VersionedProcessGroup localContents = mapper.mapProcessGroup(group, flowRegistryClient, true);
 
         final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localContents);
-        final ComparableDataFlow proposedFlow = new StandardComparableDataFlow("Proposed Flow", updatedSnapshot.getFlowContents());
+        final ComparableDataFlow proposedFlow = new StandardComparableDataFlow("Versioned Flow", updatedSnapshot.getFlowContents());
 
-        final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow);
+        final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow, new StaticDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();
 
         final Set<AffectedComponentEntity> affectedComponents = comparison.getDifferences().stream()
@@ -3958,7 +4024,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final VersionedFlowSnapshot snapshot;
         try {
             snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion());
-        } catch (final UnknownResourceException e) {
+        } catch (final NiFiRegistryException e) {
             throw new IllegalArgumentException("The Flow Registry with ID " + versionControlInfo.getRegistryId() + " reports that no Flow exists with Bucket "
                 + versionControlInfo.getBucketId() + ", Flow " + versionControlInfo.getFlowId() + ", Version " + versionControlInfo.getVersion());
         }
@@ -3969,6 +4035,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return snapshot;
     }
 
+    @Override
+    public String getFlowRegistryName(final String flowRegistryId) {
+        final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(flowRegistryId);
+        return flowRegistry == null ? flowRegistryId : flowRegistry.getName();
+    }
+
     private void populateVersionedChildFlows(final VersionedFlowSnapshot snapshot) throws IOException {
         final VersionedProcessGroup group = snapshot.getFlowContents();
 
@@ -3993,7 +4065,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             final VersionedFlowSnapshot childSnapshot;
             try {
                 childSnapshot = flowRegistry.getFlowContents(remoteCoordinates.getBucketId(), remoteCoordinates.getFlowId(), remoteCoordinates.getVersion());
-            } catch (final UnknownResourceException e) {
+            } catch (final NiFiRegistryException e) {
                 throw new IllegalArgumentException("The Flow Registry with ID " + registryId + " reports that no Flow exists with Bucket "
                     + remoteCoordinates.getBucketId() + ", Flow " + remoteCoordinates.getFlowId() + ", Version " + remoteCoordinates.getVersion());
             }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index b8bdc14..6bf4cca 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -39,6 +39,7 @@ import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.nar.NarClassLoaders;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.IllegalClusterResourceRequestException;
 import org.apache.nifi.web.NiFiServiceFacade;
@@ -1373,7 +1374,7 @@ public class FlowResource extends ApplicationResource {
                     value = "The registry id.",
                     required = true
             )
-            @PathParam("id") String id) {
+            @PathParam("id") String id) throws NiFiRegistryException {
 
         authorizeFlow();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 11c548f..d24dcbb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -87,8 +87,10 @@ import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.serialization.FlowEncodingVersion;
 import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.FlowRegistryUtils;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateStep;
 import org.apache.nifi.remote.util.SiteToSiteRestApiClient;
@@ -123,6 +125,7 @@ import org.apache.nifi.web.api.entity.ControllerServicesEntity;
 import org.apache.nifi.web.api.entity.CopySnippetRequestEntity;
 import org.apache.nifi.web.api.entity.CreateTemplateRequestEntity;
 import org.apache.nifi.web.api.entity.Entity;
+import org.apache.nifi.web.api.entity.FlowComparisonEntity;
 import org.apache.nifi.web.api.entity.FlowEntity;
 import org.apache.nifi.web.api.entity.FunnelEntity;
 import org.apache.nifi.web.api.entity.FunnelsEntity;
@@ -301,6 +304,53 @@ public class ProcessGroupResource extends ApplicationResource {
 
 
     /**
+     * Retrieves a list of local modifications to the Process Group since it was last synchronized with the Flow Registry
+     *
+     * @param groupId The id of the process group.
+     * @return A processGroupEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}/local-modifications")
+    @ApiOperation(
+            value = "Gets a list of local modifications to the Process Group since it was last synchronized with the Flow Registry",
+            response = FlowComparisonEntity.class,
+            authorizations = {
+            @Authorization(value = "Read - /process-groups/{uuid}"),
+            @Authorization(value = "Read - /{component-type}/{uuid} - For all encapsulated components")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                    @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                    @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                    @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response getLocalModifications(
+            @ApiParam(
+                    value = "The process group id.",
+                    required = false
+            )
+            @PathParam("id") final String groupId) throws IOException, NiFiRegistryException {
+
+        // authorize access
+        serviceFacade.authorizeAccess(lookup -> {
+            final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
+            final Authorizable processGroup = groupAuthorizable.getAuthorizable();
+            processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+            super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, false, false, true, false);
+        });
+
+        final FlowComparisonEntity entity = serviceFacade.getLocalModifications(groupId);
+        return generateOkResponse(entity).build();
+    }
+
+
+    /**
      * Retrieves the Variable Registry for the group with the given ID
      *
      * @param groupId the ID of the Process Group
@@ -1594,6 +1644,13 @@ public class ProcessGroupResource extends ApplicationResource {
             // Step 2: Retrieve flow from Flow Registry
             final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(versionControlInfo);
 
+            final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
+            versionControlInfo.setBucketName(metadata.getBucketName());
+            versionControlInfo.setFlowName(metadata.getFlowName());
+            versionControlInfo.setFlowDescription(metadata.getFlowDescription());
+
+            versionControlInfo.setRegistryName(serviceFacade.getFlowRegistryName(versionControlInfo.getRegistryId()));
+
             // Step 3: Resolve Bundle info
             BundleUtils.discoverCompatibleBundles(flowSnapshot.getFlowContents());
 
@@ -1635,14 +1692,14 @@ public class ProcessGroupResource extends ApplicationResource {
 
                     // create the process group contents
                     final Revision revision = getRevision(processGroupGroupEntity, processGroupGroupEntity.getComponent().getId());
-                    final ProcessGroupEntity entity = serviceFacade.createProcessGroup(revision, groupId, processGroupGroupEntity.getComponent());
+                    ProcessGroupEntity entity = serviceFacade.createProcessGroup(revision, groupId, processGroupGroupEntity.getComponent());
 
                     final VersionedFlowSnapshot flowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
                     if (flowSnapshot != null) {
                         final RevisionDTO revisionDto = entity.getRevision();
                         final String newGroupId = entity.getComponent().getId();
                         final Revision newGroupRevision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), newGroupId);
-                        serviceFacade.updateProcessGroupContents(NiFiUserUtils.getNiFiUser(), newGroupRevision, newGroupId,
+                        entity = serviceFacade.updateProcessGroupContents(NiFiUserUtils.getNiFiUser(), newGroupRevision, newGroupId,
                             versionControlInfo, flowSnapshot, getIdGenerationSeed().orElse(null), false, false);
                     }
 


[19/50] nifi git commit: NIFI-4436: - Clearing bucket/flow/versions when changing the selected registry/bucket. - Using the versioned flow to get the group name when importing. - Adding menu items for viewing local changes. - Showing local changes during

Posted by bb...@apache.org.
NIFI-4436:
- Clearing bucket/flow/versions when changing the selected registry/bucket.
- Using the versioned flow to get the group name when importing.
- Adding menu items for viewing local changes.
- Showing local changes during revert request.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/3d8b1e48
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/3d8b1e48
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/3d8b1e48

Branch: refs/heads/master
Commit: 3d8b1e4890d5802677e978a6a7588a97bf4177f2
Parents: f6cc5b6
Author: Matt Gilman <ma...@gmail.com>
Authored: Thu Nov 16 14:41:41 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:53 2018 -0500

----------------------------------------------------------------------
 .../web/api/dto/ComponentDifferenceDTO.java     |  13 +-
 .../apache/nifi/web/api/dto/DifferenceDTO.java  |  47 ++
 .../nifi/web/api/ProcessGroupResource.java      | 138 ++--
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  66 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |   4 +-
 .../src/main/webapp/WEB-INF/pages/canvas.jsp    |   2 +
 .../canvas/revert-local-changes-dialog.jsp      |  36 +
 .../canvas/show-local-changes-dialog.jsp        |  35 +
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |  17 +
 .../header/components/nf-ng-group-component.js  |  35 +-
 .../src/main/webapp/js/nf/canvas/nf-actions.js  |  12 +
 .../main/webapp/js/nf/canvas/nf-context-menu.js | 124 ++-
 .../main/webapp/js/nf/canvas/nf-flow-version.js | 800 ++++++++++++++-----
 13 files changed, 1008 insertions(+), 321 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
index 8febce0..e45867d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDifferenceDTO.java
@@ -17,12 +17,11 @@
 
 package org.apache.nifi.web.api.dto;
 
-import java.util.List;
-import java.util.Objects;
+import io.swagger.annotations.ApiModelProperty;
 
 import javax.xml.bind.annotation.XmlType;
-
-import io.swagger.annotations.ApiModelProperty;
+import java.util.List;
+import java.util.Objects;
 
 @XmlType(name = "componentDifference")
 public class ComponentDifferenceDTO {
@@ -30,7 +29,7 @@ public class ComponentDifferenceDTO {
     private String componentId;
     private String componentName;
     private String processGroupId;
-    private List<String> differences;
+    private List<DifferenceDTO> differences;
 
     @ApiModelProperty("The type of component")
     public String getComponentType() {
@@ -69,11 +68,11 @@ public class ComponentDifferenceDTO {
     }
 
     @ApiModelProperty("The differences in the component between the two flows")
-    public List<String> getDifferences() {
+    public List<DifferenceDTO> getDifferences() {
         return differences;
     }
 
-    public void setDifferences(List<String> differences) {
+    public void setDifferences(List<DifferenceDTO> differences) {
         this.differences = differences;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
new file mode 100644
index 0000000..9f06149
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
@@ -0,0 +1,47 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "difference")
+public class DifferenceDTO {
+    private String differenceType;
+    private String difference;
+
+    @ApiModelProperty("The type of difference")
+    public String getDifferenceType() {
+        return differenceType;
+    }
+
+    public void setDifferenceType(String differenceType) {
+        this.differenceType = differenceType;
+    }
+
+    @ApiModelProperty("Description of the difference")
+    public String getDifference() {
+        return difference;
+    }
+
+    public void setDifference(String difference) {
+        this.difference = difference;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index fe57dd6..de56a4f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -16,57 +16,12 @@
  */
 package org.apache.nifi.web.api;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.stream.XMLStreamReader;
-
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeAccess;
@@ -156,12 +111,55 @@ import org.slf4j.LoggerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * RESTful endpoint for managing a Group.
@@ -341,9 +339,7 @@ public class ProcessGroupResource extends ApplicationResource {
         // authorize access
         serviceFacade.authorizeAccess(lookup -> {
             final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
-            final Authorizable processGroup = groupAuthorizable.getAuthorizable();
-            processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-            super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, false, false, true, false);
+            authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, false, false, true, false);
         });
 
         final FlowComparisonEntity entity = serviceFacade.getLocalModifications(groupId);
@@ -1625,6 +1621,11 @@ public class ProcessGroupResource extends ApplicationResource {
             }
         }
 
+        // if the group name isn't specified, ensure the group is being imported from version control
+        if (StringUtils.isBlank(requestProcessGroupEntity.getComponent().getName()) && requestProcessGroupEntity.getComponent().getVersionControlInformation() == null) {
+            throw new IllegalArgumentException("The group name is required when the group is not imported from version control.");
+        }
+
         if (requestProcessGroupEntity.getComponent().getParentGroupId() != null && !groupId.equals(requestProcessGroupEntity.getComponent().getParentGroupId())) {
             throw new IllegalArgumentException(String.format("If specified, the parent process group id %s must be the same as specified in the URI %s",
                     requestProcessGroupEntity.getComponent().getParentGroupId(), groupId));
@@ -1688,15 +1689,22 @@ public class ProcessGroupResource extends ApplicationResource {
                         serviceFacade.verifyComponentTypes(versionedFlowSnapshot.getFlowContents());
                     }
                 },
-                processGroupGroupEntity -> {
+                processGroupEntity -> {
+                    final ProcessGroupDTO processGroup = processGroupEntity.getComponent();
+
                     // set the processor id as appropriate
-                    processGroupGroupEntity.getComponent().setId(generateUuid());
+                    processGroup.setId(generateUuid());
+
+                    // ensure the group name comes from the versioned flow
+                    final VersionedFlowSnapshot flowSnapshot = processGroupEntity.getVersionedFlowSnapshot();
+                    if (flowSnapshot != null && StringUtils.isNotBlank(flowSnapshot.getFlowContents().getName()) && StringUtils.isBlank(processGroup.getName())) {
+                        processGroup.setName(flowSnapshot.getFlowContents().getName());
+                    }
 
                     // create the process group contents
-                    final Revision revision = getRevision(processGroupGroupEntity, processGroupGroupEntity.getComponent().getId());
-                    ProcessGroupEntity entity = serviceFacade.createProcessGroup(revision, groupId, processGroupGroupEntity.getComponent());
+                    final Revision revision = getRevision(processGroupEntity, processGroup.getId());
+                    ProcessGroupEntity entity = serviceFacade.createProcessGroup(revision, groupId, processGroup);
 
-                    final VersionedFlowSnapshot flowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
                     if (flowSnapshot != null) {
                         final RevisionDTO revisionDto = entity.getRevision();
                         final String newGroupId = entity.getComponent().getId();

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 1a12dcf..6077268 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,33 +16,6 @@
  */
 package org.apache.nifi.web.api.dto;
 
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.WebApplicationException;
-
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -215,6 +188,32 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
+import javax.ws.rs.WebApplicationException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
 public final class DtoFactory {
 
     @SuppressWarnings("rawtypes")
@@ -2181,15 +2180,20 @@ public final class DtoFactory {
 
 
     public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison) {
-        final Map<ComponentDifferenceDTO, List<String>> differencesByComponent = new HashMap<>();
+        final Map<ComponentDifferenceDTO, List<DifferenceDTO>> differencesByComponent = new HashMap<>();
 
         for (final FlowDifference difference : comparison.getDifferences()) {
             final ComponentDifferenceDTO componentDiff = createComponentDifference(difference);
-            final List<String> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
-            differences.add(difference.getDescription());
+            final List<DifferenceDTO> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
+
+            final DifferenceDTO dto = new DifferenceDTO();
+            dto.setDifferenceType(difference.getDifferenceType().getDescription());
+            dto.setDifference(difference.getDescription());
+
+            differences.add(dto);
         }
 
-        for (final Map.Entry<ComponentDifferenceDTO, List<String>> entry : differencesByComponent.entrySet()) {
+        for (final Map.Entry<ComponentDifferenceDTO, List<DifferenceDTO>> entry : differencesByComponent.entrySet()) {
             entry.getKey().setDifferences(entry.getValue());
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 78f3e31..e3c4725 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -61,7 +61,9 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
 
         // create the process group
         ProcessGroup group = flowController.createProcessGroup(processGroup.getId());
-        group.setName(processGroup.getName());
+        if (processGroup.getName() != null) {
+            group.setName(processGroup.getName());
+        }
         if (processGroup.getPosition() != null) {
             group.setPosition(new Position(processGroup.getPosition().getX(), processGroup.getPosition().getY()));
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
index 258bdad..52fc43c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
@@ -117,6 +117,8 @@
         <jsp:include page="/WEB-INF/partials/canvas/connections-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/save-flow-version-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/import-flow-version-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/show-local-changes-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/registry-configuration-dialog.jsp"/>
         <div id="canvas-container" class="unselectable"></div>
         <div id="canvas-tooltips">

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
new file mode 100644
index 0000000..45d4c4c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
@@ -0,0 +1,36 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="revert-local-changes-dialog" layout="column" class="hidden large-dialog">
+    <div class="dialog-content">
+        <div class="setting">
+            <div class="setting-field">
+                Are you sure you want to revert changes? All flow configuration changes detailed below will be reverted to the last version.
+            </div>
+        </div>
+        <span id="revert-local-changes-process-group-id" class="hidden"></span>
+        <div id="revert-local-changes-filter-controls">
+            <div id="revert-local-changes-filter-status" class="filter-status">
+                Displaying&nbsp;<span id="displayed-revert-local-changes-entries"></span>&nbsp;of&nbsp;<span id="total-revert-local-changes-entries"></span>
+            </div>
+            <div id="revert-local-changes-filter-container">
+                <input type="text" id="revert-local-changes-filter" placeholder="Filter"/>
+            </div>
+        </div>
+        <div id="revert-local-changes-table"></div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
new file mode 100644
index 0000000..9b122ae
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
@@ -0,0 +1,35 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="show-local-changes-dialog" layout="column" class="hidden large-dialog">
+    <div class="dialog-content">
+        <div class="setting">
+            <div class="setting-field">
+                The following changes have been made to the flow since the last version.
+            </div>
+        </div>
+        <div id="show-local-changes-filter-controls">
+            <div id="show-local-changes-filter-status" class="filter-status">
+                Displaying&nbsp;<span id="displayed-show-local-changes-entries"></span>&nbsp;of&nbsp;<span id="total-show-local-changes-entries"></span>
+            </div>
+            <div id="show-local-changes-filter-container">
+                <input type="text" id="show-local-changes-filter" placeholder="Filter"/>
+            </div>
+        </div>
+        <div id="show-local-changes-table"></div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index 29a8c0e..69d4bea 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -264,6 +264,23 @@ div.progress-label {
 }
 
 /*
+    Local changes
+ */
+
+#revert-local-changes-table, #show-local-changes-table {
+    position: absolute;
+    top: 80px;
+    left: 0px;
+    right: 0px;
+    bottom: 0px;
+    height: 225px;
+}
+
+#revert-local-changes-filter, #show-local-changes-filter {
+    width: 173px;
+}
+
+/*
     Variable Registry
  */
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
index ae9e228..2ce8438 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/header/components/nf-ng-group-component.js
@@ -25,9 +25,10 @@
                 'nf.Graph',
                 'nf.CanvasUtils',
                 'nf.ErrorHandler',
-                'nf.Common'],
-            function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon) {
-                return (nf.ng.GroupComponent = factory($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon));
+                'nf.Common',
+                'nf.Dialog'],
+            function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon, nfDialog) {
+                return (nf.ng.GroupComponent = factory($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon, nfDialog));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.ng.GroupComponent =
@@ -37,7 +38,8 @@
                 require('nf.Graph'),
                 require('nf.CanvasUtils'),
                 require('nf.ErrorHandler'),
-                require('nf.Common')));
+                require('nf.Common'),
+                require('nf.Dialog')));
     } else {
         nf.ng.GroupComponent = factory(root.$,
             root.nf.Client,
@@ -45,9 +47,10 @@
             root.nf.Graph,
             root.nf.CanvasUtils,
             root.nf.ErrorHandler,
-            root.nf.Common);
+            root.nf.Common,
+            root.nf.Dialog);
     }
-}(this, function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon) {
+}(this, function ($, nfClient, nfBirdseye, nfGraph, nfCanvasUtils, nfErrorHandler, nfCommon, nfDialog) {
     'use strict';
 
     return function (serviceProvider) {
@@ -236,12 +239,22 @@
                         // hide the dialog
                         groupComponent.modal.hide();
 
-                        // create the group and resolve the deferred accordingly
-                        createGroup(groupName, pt).done(function (response) {
-                            deferred.resolve(response.component);
-                        }).fail(function () {
+                        // ensure the group name is specified
+                        if (nfCommon.isBlank(groupName)) {
+                            nfDialog.showOkDialog({
+                                headerText: 'Create Process Group',
+                                dialogContent: 'The group name is required.'
+                            });
+
                             deferred.reject();
-                        });
+                        } else {
+                            // create the group and resolve the deferred accordingly
+                            createGroup(groupName, pt).done(function (response) {
+                                deferred.resolve(response.component);
+                            }).fail(function () {
+                                deferred.reject();
+                            });
+                        }
                     };
 
                     groupComponent.modal.update('setButtonModel', [{

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
index 9dc20b1..725b6a1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
@@ -1266,6 +1266,18 @@
         },
 
         /**
+         * Shows local changes.
+         */
+        showLocalChanges: function (selection) {
+            if (selection.empty()) {
+                nfFlowVersion.showLocalChanges(nfCanvasUtils.getGroupId());
+            } else if (selection.size() === 1) {
+                var selectionData = selection.datum();
+                nfFlowVersion.showLocalChanges(selectionData.id)
+            }
+        },
+
+        /**
          * Changes the flow version.
          */
         changeFlowVersion: function (selection) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index 8656035..616e151 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -428,6 +428,123 @@
     };
 
     /**
+     * Returns whether the process group support supports commit.
+     *
+     * @param selection
+     * @returns {boolean}
+     */
+    var supportsCommitFlowVersion = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        var versionControlInformation;
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    versionControlInformation = breadcrumbEntity.breadcrumb.versionControlInformation;
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        } else {
+            var processGroupData = selection.datum();
+            versionControlInformation = processGroupData.component.versionControlInformation;
+        }
+
+        if (nfCommon.isUndefinedOrNull(versionControlInformation)) {
+            return false;
+        }
+
+        // check the selection for version control information
+        return versionControlInformation.current === true && versionControlInformation.modified === true;
+    };
+
+    /**
+     * Returns whether the process group supports revert local changes.
+     *
+     * @param selection
+     * @returns {boolean}
+     */
+    var hasLocalChanges = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        var versionControlInformation;
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    versionControlInformation = breadcrumbEntity.breadcrumb.versionControlInformation;
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        } else {
+            var processGroupData = selection.datum();
+            versionControlInformation = processGroupData.component.versionControlInformation;
+        }
+
+        if (nfCommon.isUndefinedOrNull(versionControlInformation)) {
+            return false;
+        }
+
+        // check the selection for version control information
+        return versionControlInformation.modified === true;
+    };
+
+    /**
+     * Returns whether the process group supports changing the flow version.
+     *
+     * @param selection
+     * @returns {boolean}
+     */
+    var supportsChangeFlowVersion = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        var versionControlInformation;
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    versionControlInformation = breadcrumbEntity.breadcrumb.versionControlInformation;
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        } else {
+            var processGroupData = selection.datum();
+            versionControlInformation = processGroupData.component.versionControlInformation;
+        }
+
+        if (nfCommon.isUndefinedOrNull(versionControlInformation)) {
+            return false;
+        }
+
+        // check the selection for version control information
+        return versionControlInformation.modified === false;
+    };
+
+    /**
      * Determines whether the current selection supports stopping flow versioning.
      *
      * @param selection
@@ -640,9 +757,10 @@
         {id: 'version-menu-item', groupMenuItem: {clazz: 'fa', text: 'Version'}, menuItems: [
             {id: 'start-version-control-menu-item', condition: supportsStartFlowVersioning, menuItem: {clazz: 'fa fa-upload', text: 'Start version control', action: 'saveFlowVersion'}},
             {separator: true},
-            {id: 'commit-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-upload', text: 'Commit local changes', action: 'saveFlowVersion'}},
-            {id: 'revert-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-undo', text: 'Revert local changes', action: 'revertLocalChanges'}},
-            {id: 'change-version-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Change version', action: 'changeFlowVersion'}},
+            {id: 'commit-menu-item', condition: supportsCommitFlowVersion, menuItem: {clazz: 'fa fa-upload', text: 'Commit local changes', action: 'saveFlowVersion'}},
+            {id: 'local-changes-menu-item', condition: hasLocalChanges, menuItem: {clazz: 'fa', text: 'Show local changes', action: 'showLocalChanges'}},
+            {id: 'revert-menu-item', condition: hasLocalChanges, menuItem: {clazz: 'fa fa-undo', text: 'Revert local changes', action: 'revertLocalChanges'}},
+            {id: 'change-version-menu-item', condition: supportsChangeFlowVersion, menuItem: {clazz: 'fa', text: 'Change version', action: 'changeFlowVersion'}},
             {separator: true},
             {id: 'stop-version-control-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Stop version control', action: 'stopVersionControl'}}
         ]},

http://git-wip-us.apache.org/repos/asf/nifi/blob/3d8b1e48/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 4eb5286..28e4303 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -30,10 +30,11 @@
                 'nf.Client',
                 'nf.CanvasUtils',
                 'nf.ProcessGroup',
+                'nf.ProcessGroupConfiguration',
                 'nf.Graph',
                 'nf.Birdseye'],
-            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph, nfBirdseye) {
-                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph, nfBirdseye));
+            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfProcessGroupConfiguration, nfGraph, nfBirdseye) {
+                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfProcessGroupConfiguration, nfGraph, nfBirdseye));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.FlowVerison =
@@ -45,6 +46,7 @@
                 require('nf.Client'),
                 require('nf.CanvasUtils'),
                 require('nf.ProcessGroup'),
+                require('nf.ProcessGroupConfiguration'),
                 require('nf.Graph'),
                 require('nf.Birdseye')));
     } else {
@@ -56,10 +58,11 @@
             root.nf.Client,
             root.nf.CanvasUtils,
             root.nf.ProcessGroup,
+            root.nf.ProcessGroupConfiguration,
             root.nf.Graph,
             root.nf.Birdseye);
     }
-}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph, nfBirdseye) {
+}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfProcessGroupConfiguration, nfGraph, nfBirdseye) {
     'use strict';
 
     var serverTimeOffset = null;
@@ -97,6 +100,55 @@
     };
 
     /**
+     * Reset the revert local changes dialog.
+     */
+    var resetRevertLocalChangesDialog = function () {
+        $('#revert-local-changes-process-group-id').text('');
+
+        clearLocalChangesGrid($('#revert-local-changes-table'), $('#revert-local-changes-filter'), $('#displayed-revert-local-changes-entries'), $('#total-revert-local-changes-entries'));
+    };
+
+    /**
+     * Reset the show local changes dialog.
+     */
+    var resetShowLocalChangesDialog = function () {
+        clearLocalChangesGrid($('#show-local-changes-table'), $('#show-local-changes-filter'), $('#displayed-show-local-changes-entries'), $('#total-show-local-changes-entries'));
+    };
+
+    /**
+     * Clears the local changes grid.
+     */
+    var clearLocalChangesGrid = function (localChangesTable, filterInput, displayedLabel, totalLabel) {
+        var localChangesGrid = localChangesTable.data('gridInstance');
+        if (nfCommon.isDefinedAndNotNull(localChangesGrid)) {
+            localChangesGrid.setSelectedRows([]);
+            localChangesGrid.resetActiveCell();
+
+            var localChangesData = localChangesGrid.getData();
+            localChangesData.setItems([]);
+        }
+
+        filterInput.val('');
+
+        displayedLabel.text('0');
+        totalLabel.text('0');
+    }
+
+    /**
+     * Clears the version grid
+     */
+    var clearFlowVersionsGrid = function () {
+        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
+        if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) {
+            importFlowVersionGrid.setSelectedRows([]);
+            importFlowVersionGrid.resetActiveCell();
+
+            var importFlowVersionData = importFlowVersionGrid.getData();
+            importFlowVersionData.setItems([]);
+        }
+    };
+
+    /**
      * Reset the import flow version dialog.
      */
     var resetImportFlowVersionDialog = function () {
@@ -110,14 +162,7 @@
         $('#import-flow-version-bucket').text('').hide();
         $('#import-flow-version-name').text('').hide();
 
-        var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
-        if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) {
-            importFlowVersionGrid.setSelectedRows([]);
-            importFlowVersionGrid.resetActiveCell();
-
-            var importFlowVersionData = importFlowVersionGrid.getData();
-            importFlowVersionData.setItems([]);
-        }
+        clearFlowVersionsGrid();
 
         $('#import-flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
 
@@ -131,9 +176,10 @@
      * @param dialog
      * @param registryCombo
      * @param bucketCombo
+     * @param flowCombo
      * @returns {deferred}
      */
-    var loadRegistries = function (dialog, registryCombo, bucketCombo, selectBucket) {
+    var loadRegistries = function (dialog, registryCombo, bucketCombo, flowCombo, selectBucket) {
         return $.ajax({
             type: 'GET',
             url: '../nifi-api/flow/registries',
@@ -167,7 +213,7 @@
             registryCombo.combo({
                 options: registries,
                 select: function (selectedOption) {
-                    selectRegistry(dialog, selectedOption, bucketCombo, selectBucket)
+                    selectRegistry(dialog, selectedOption, bucketCombo, flowCombo, selectBucket)
                 }
             });
         }).fail(nfErrorHandler.handleAjaxError);
@@ -178,6 +224,7 @@
      *
      * @param registryIdentifier
      * @param bucketCombo
+     * @param selectBucket
      * @returns {*}
      */
     var loadBuckets = function (registryIdentifier, bucketCombo, selectBucket) {
@@ -224,9 +271,10 @@
      * @param dialog
      * @param selectedOption
      * @param bucketCombo
+     * @param flowCombo
      * @param selectBucket
      */
-    var selectRegistry = function (dialog, selectedOption, bucketCombo, selectBucket) {
+    var selectRegistry = function (dialog, selectedOption, bucketCombo, flowCombo, selectBucket) {
         var showNoBucketsAvailable = function () {
             bucketCombo.combo('destroy').combo({
                 options: [{
@@ -243,6 +291,28 @@
         if (selectedOption.disabled === true) {
             showNoBucketsAvailable();
         } else {
+            bucketCombo.combo('destroy').combo({
+                options: [{
+                    text: 'Loading buckets...',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
+
+            if (nfCommon.isDefinedAndNotNull(flowCombo)) {
+                flowCombo.combo('destroy').combo({
+                    options: [{
+                        text: 'Loading flows...',
+                        value: null,
+                        optionClass: 'unset',
+                        disabled: true
+                    }]
+                });
+
+                clearFlowVersionsGrid();
+            }
+
             loadBuckets(selectedOption.value, bucketCombo, selectBucket).fail(function () {
                 showNoBucketsAvailable();
             });
@@ -344,7 +414,7 @@
             return nfCommon.formatDateTime(date);
         };
 
-        // define the column model for the controller services table
+        // define the column model for flow versions
         var importFlowVersionColumns = [
             {
                 id: 'version',
@@ -427,6 +497,185 @@
     };
 
     /**
+     * Initializes the specified local changes table.
+     *
+     * @param localChangesTable
+     * @param filterInput
+     * @param displayedLabel
+     * @param totalLabel
+     */
+    var initLocalChangesTable = function (localChangesTable, filterInput, displayedLabel, totalLabel) {
+
+        var getFilterText = function () {
+            return filterInput.val();
+        };
+
+        var applyFilter = function () {
+            // get the dataview
+            var localChangesGrid = localChangesTable.data('gridInstance');
+
+            // ensure the grid has been initialized
+            if (nfCommon.isDefinedAndNotNull(localChangesGrid)) {
+                var localChangesData = localChangesGrid.getData();
+
+                // update the search criteria
+                localChangesData.setFilterArgs({
+                    searchString: getFilterText()
+                });
+                localChangesData.refresh();
+            }
+        };
+
+        var filter = function (item, args) {
+            if (args.searchString === '') {
+                return true;
+            }
+
+            try {
+                // perform the row filtering
+                var filterExp = new RegExp(args.searchString, 'i');
+            } catch (e) {
+                // invalid regex
+                return false;
+            }
+
+            // determine if the item matches the filter
+            var matchesId = item['componentId'].search(filterExp) >= 0;
+            var matchesComponent = item['componentName'].search(filterExp) >= 0;
+            var matchesDifferenceType = item['differenceType'].search(filterExp) >= 0;
+            var matchesDifference = item['difference'].search(filterExp) >= 0;
+
+            return matchesId || matchesComponent || matchesDifferenceType || matchesDifference;
+        };
+
+        // initialize the component state filter
+        filterInput.on('keyup', function () {
+            applyFilter();
+        });
+
+        var valueFormatter = function (row, cell, value, columnDef, dataContext) {
+            return nfCommon.escapeHtml(value);
+        };
+
+        var actionsFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            if (dataContext.differenceType !== 'Component Removed' && nfCommon.isDefinedAndNotNull(dataContext.processGroupId)) {
+                markup += '<div class="pointer go-to-component fa fa-long-arrow-right" title="Go To" style="margin-top: 2px" ></div>';
+            }
+
+            return markup;
+        };
+
+        // define the column model for local changes
+        var localChangesColumns = [
+            {
+                id: 'component',
+                name: 'Component Name',
+                field: 'componentName',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'differenceType',
+                name: 'Type',
+                field: 'differenceType',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'difference',
+                name: 'Difference',
+                field: 'difference',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true,
+                minWidth: 300
+            },
+            {
+                id: 'actions',
+                name: '&nbsp;',
+                formatter: actionsFormatter,
+                sortable: false,
+                resizable: false,
+                width: 25
+            }
+        ];
+
+        // initialize the dataview
+        var localChangesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        localChangesData.setFilterArgs({
+            searchString: '',
+            property: 'component'
+        });
+        localChangesData.setFilter(filter);
+
+        // initialize the sort
+        sort({
+            columnId: 'version',
+            sortAsc: false
+        }, localChangesData);
+
+        // initialize the grid
+        var localChangesGrid = new Slick.Grid(localChangesTable, localChangesData, localChangesColumns, gridOptions);
+        localChangesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        localChangesGrid.registerPlugin(new Slick.AutoTooltips());
+        localChangesGrid.setSortColumn('version', false);
+        localChangesGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.id,
+                sortAsc: args.sortAsc
+            }, localChangesData);
+        });
+
+        // configure a click listener
+        localChangesGrid.onClick.subscribe(function (e, args) {
+            var target = $(e.target);
+
+            // get the node at this row
+            var componentDifference = localChangesData.getItem(args.row);
+
+            // determine the desired action
+            if (localChangesGrid.getColumns()[args.cell].id === 'actions') {
+                if (target.hasClass('go-to-component')) {
+                    if (componentDifference.componentType === 'Controller Service') {
+                        nfProcessGroupConfiguration.showConfiguration(componentDifference.processGroupId).done(function () {
+                            nfProcessGroupConfiguration.selectControllerService(componentDifference.componentId);
+                        });
+                    } else {
+                        nfCanvasUtils.showComponent(componentDifference.processGroupId, componentDifference.componentId);
+                    }
+                }
+            }
+        });
+
+        // wire up the dataview to the grid
+        localChangesData.onRowCountChanged.subscribe(function (e, args) {
+            localChangesGrid.updateRowCount();
+            localChangesGrid.render();
+
+            // update the total number of displayed items
+            displayedLabel.text(nfCommon.formatInteger(args.current));
+        });
+        localChangesData.onRowsChanged.subscribe(function (e, args) {
+            localChangesGrid.invalidateRows(args.rows);
+            localChangesGrid.render();
+        });
+        localChangesData.syncGridSelection(localChangesGrid, true);
+
+        // hold onto an instance of the grid
+        localChangesTable.data('gridInstance', localChangesGrid);
+
+        // initialize the number of display items
+        displayedLabel.text('0');
+        totalLabel.text('0');
+    };
+
+    /**
      * Shows the import flow version dialog.
      */
     var showImportFlowVersionDialog = function () {
@@ -450,7 +699,7 @@
                 disabled: true
             }]
         }).show();
-        $('#import-flow-version-name-combo').combo('destroy').combo({
+        var flowCombo = $('#import-flow-version-name-combo').combo('destroy').combo({
             options: [{
                 text: 'Loading flows...',
                 value: null,
@@ -459,7 +708,7 @@
             }]
         }).show();
 
-        loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, selectBucketImportVersion).done(function () {
+        loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, flowCombo, selectBucketImportVersion).done(function () {
             // show the import dialog
             $('#import-flow-version-dialog').modal('setHeaderText', 'Import Version').modal('setButtonModel', [{
                 buttonText: 'Import',
@@ -619,6 +868,19 @@
      * @param selectedBucket
      */
     var selectBucketImportVersion = function (selectedBucket) {
+        // mark the flows as loading
+        $('#import-flow-version-name-combo').combo('destroy').combo({
+            options: [{
+                text: 'Loading flows...',
+                value: null,
+                optionClass: 'unset',
+                disabled: true
+            }]
+        });
+
+        // clear the flow versions grid
+        clearFlowVersionsGrid();
+
         var selectedRegistry = $('#import-flow-version-registry-combo').combo('getSelectedOption');
 
         // load the flows for the currently selected registry and bucket
@@ -646,7 +908,6 @@
                 }
             }),
             'component': {
-                'name': selectedFlow.text, // TODO - name from versioned PG?
                 'position': {
                     'x': pt.x,
                     'y': pt.y
@@ -966,6 +1227,240 @@
         progressBar.append(label);
     };
 
+    /**
+     * Shows local changes for the specified process group.
+     *
+     * @param processGroupId
+     * @param localChangesTable
+     * @param totalLabel
+     */
+    var loadLocalChanges = function (processGroupId, localChangesTable, totalLabel) {
+        var localChangesGrid = localChangesTable.data('gridInstance');
+        var localChangesData = localChangesGrid.getData();
+
+        // begin the update
+        localChangesData.beginUpdate();
+
+        // remove the current versions
+        localChangesGrid.setSelectedRows([]);
+        localChangesGrid.resetActiveCell();
+        localChangesData.setItems([]);
+
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/local-modifications',
+            dataType: 'json'
+        }).done(function (response) {
+            if (nfCommon.isDefinedAndNotNull(response.componentDifferences) && response.componentDifferences.length > 0) {
+                var totalDifferences = 0;
+                $.each(response.componentDifferences, function (_, componentDifference) {
+                    $.each(componentDifference.differences, function (_, difference) {
+                        localChangesData.addItem({
+                            id: totalDifferences++,
+                            componentId: componentDifference.componentId,
+                            componentName: componentDifference.componentName,
+                            componentType: componentDifference.componentType,
+                            processGroupId: componentDifference.processGroupId,
+                            differenceType: difference.differenceType,
+                            difference: difference.difference
+                        });
+                    });
+                });
+
+                // end the update
+                localChangesData.endUpdate();
+
+                // resort
+                localChangesData.reSort();
+                localChangesGrid.invalidate();
+
+                // update the total displayed
+                totalLabel.text(nfCommon.formatInteger(totalDifferences));
+            } else {
+                nfDialog.showOkDialog({
+                    headerText: 'Local Changes',
+                    dialogContent: 'This Process Group does not have any local changes.'
+                });
+            }
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Revert local changes for the specified process group.
+     *
+     * @param processGroupId
+     */
+    var revertLocalChanges = function (processGroupId) {
+        getVersionControlInformation(processGroupId).done(function (response) {
+            if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                var revertTimer = null;
+                var revertRequest = null;
+                var cancelled = false;
+
+                // update the button model of the revert status dialog
+                $('#change-version-status-dialog').modal('setButtonModel', [{
+                    buttonText: 'Stop',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    handler: {
+                        click: function () {
+                            cancelled = true;
+
+                            $('#change-version-status-dialog').modal('setButtonModel', []);
+
+                            // we are waiting for the next poll attempt
+                            if (revertTimer !== null) {
+                                // cancel it
+                                clearTimeout(revertTimer);
+
+                                // cancel the revert request
+                                completeRevertRequest();
+                            }
+                        }
+                    }
+                }]);
+
+                // hide the import dialog immediately
+                $('#import-flow-version-dialog').modal('hide');
+
+                var submitRevertRequest = function () {
+                    var revertFlowVersionRequest = {
+                        'processGroupRevision': nfClient.getRevision({
+                            'revision': {
+                                'version': response.processGroupRevision.version
+                            }
+                        }),
+                        'versionControlInformation': response.versionControlInformation
+                    };
+
+                    return $.ajax({
+                        type: 'POST',
+                        data: JSON.stringify(revertFlowVersionRequest),
+                        url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
+                        dataType: 'json',
+                        contentType: 'application/json'
+                    }).done(function () {
+                        // initialize the progress bar value
+                        updateProgress(0);
+
+                        // show the progress dialog
+                        $('#change-version-status-dialog').modal('show');
+                    }).fail(nfErrorHandler.handleAjaxError);
+                };
+
+                var pollRevertRequest = function () {
+                    getRevertRequest().done(processRevertResponse);
+                };
+
+                var getRevertRequest = function () {
+                    return $.ajax({
+                        type: 'GET',
+                        url: revertRequest.uri,
+                        dataType: 'json'
+                    }).fail(completeRevertRequest).fail(nfErrorHandler.handleAjaxError);
+                };
+
+                var completeRevertRequest = function () {
+                    if (cancelled === true) {
+                        // update the message to indicate successful completion
+                        $('#change-version-status-message').text('The revert request has been cancelled.');
+
+                        // update the button model
+                        $('#change-version-status-dialog').modal('setButtonModel', [{
+                            buttonText: 'Close',
+                            color: {
+                                base: '#728E9B',
+                                hover: '#004849',
+                                text: '#ffffff'
+                            },
+                            handler: {
+                                click: function () {
+                                    $(this).modal('hide');
+                                }
+                            }
+                        }]);
+                    }
+
+                    if (nfCommon.isDefinedAndNotNull(revertRequest)) {
+                        $.ajax({
+                            type: 'DELETE',
+                            url: revertRequest.uri,
+                            dataType: 'json'
+                        }).done(function (response) {
+                            revertRequest = response.request;
+
+                            // update the component that was changing
+                            updateProcessGroup(processGroupId);
+
+                            if (nfCommon.isDefinedAndNotNull(revertRequest.failureReason)) {
+                                // hide the progress dialog
+                                $('#change-version-status-dialog').modal('hide');
+
+                                nfDialog.showOkDialog({
+                                    headerText: 'Revert Local Changes',
+                                    dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
+                                });
+                            } else {
+                                // update the percent complete
+                                updateProgress(revertRequest.percentCompleted);
+
+                                // update the message to indicate successful completion
+                                $('#change-version-status-message').text('This Process Group version has changed.');
+
+                                // update the button model
+                                $('#change-version-status-dialog').modal('setButtonModel', [{
+                                    buttonText: 'Close',
+                                    color: {
+                                        base: '#728E9B',
+                                        hover: '#004849',
+                                        text: '#ffffff'
+                                    },
+                                    handler: {
+                                        click: function () {
+                                            $(this).modal('hide');
+                                        }
+                                    }
+                                }]);
+                            }
+                        });
+                    }
+                };
+
+                var processRevertResponse = function (response) {
+                    revertRequest = response.request;
+
+                    if (revertRequest.complete === true || cancelled === true) {
+                        completeRevertRequest();
+                    } else {
+                        // update the percent complete
+                        updateProgress(revertRequest.percentCompleted);
+
+                        // update the status of the revert request
+                        $('#change-version-status-message').text(revertRequest.state);
+
+                        revertTimer = setTimeout(function () {
+                            // clear the timer since we've been invoked
+                            revertTimer = null;
+
+                            // poll revert request
+                            pollRevertRequest();
+                        }, 2000);
+                    }
+                };
+
+                submitRevertRequest().done(processRevertResponse);
+            } else {
+                nfDialog.showOkDialog({
+                    headerText: 'Revert Changes',
+                    dialogContent: 'This Process Group is not currently under version control.'
+                });
+            }
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
     return {
         init: function (timeOffset) {
             serverTimeOffset = timeOffset;
@@ -998,13 +1493,11 @@
                     handler: {
                         click: function () {
                             var processGroupId = $('#save-flow-version-process-group-id').text();
-                            
                             saveFlowVersion().done(function (response) {
                                 updateVersionControlInformation(processGroupId, response.versionControlInformation);
-
-                                // close the dialog
-                                $('#save-flow-version-dialog').modal('hide');
                             });
+
+                            $(this).modal('hide');
                         }
                     }
                 }, {
@@ -1049,6 +1542,69 @@
                 }
             });
 
+            // init the revert local changes dialog
+            $('#revert-local-changes-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Revert Local Changes',
+                buttons: [{
+                    buttonText: 'Revert',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    handler: {
+                        click: function () {
+                            var processGroupId = $('#revert-local-changes-process-group-id').text();
+                            revertLocalChanges(processGroupId);
+
+                            $(this).modal('hide');
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    color: {
+                        base: '#E3E8EB',
+                        hover: '#C7D2D7',
+                        text: '#004849'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+                handler: {
+                    close: function () {
+                        resetRevertLocalChangesDialog();
+                    }
+                }
+            });
+
+            // init the show local changes dialog
+            $('#show-local-changes-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Local Changes',
+                buttons: [{
+                    buttonText: 'Close',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+                handler: {
+                    close: function () {
+                        resetShowLocalChangesDialog();
+                    }
+                }
+            });
+
             // handle the click for the process group import
             $('#import-process-group-link').on('click', function() {
                 showImportFlowVersionDialog();
@@ -1056,6 +1612,8 @@
 
             // initialize the import flow version table
             initImportFlowVersionTable();
+            initLocalChangesTable($('#revert-local-changes-table'), $('#revert-local-changes-filter'), $('#displayed-revert-local-changes-entries'), $('#total-revert-local-changes-entries'));
+            initLocalChangesTable($('#show-local-changes-table'), $('#show-local-changes-filter'), $('#displayed-show-local-changes-entries'), $('#total-show-local-changes-entries'));
         },
 
         /**
@@ -1118,7 +1676,7 @@
                         // reposition the version label
                         $('#save-flow-version-label').css('margin-top', '0');
 
-                        loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, selectBucketSaveFlowVersion).done(function () {
+                        loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, null, selectBucketSaveFlowVersion).done(function () {
                             deferred.resolve();
                         }).fail(function () {
                             deferred.reject();
@@ -1144,184 +1702,20 @@
          * @param processGroupId
          */
         revertLocalChanges: function (processGroupId) {
-            // TODO update to show user the ramifications of reverting for confirmation
-
-            // prompt the user before reverting
-            nfDialog.showYesNoDialog({
-                headerText: 'Revert Changes',
-                dialogContent: 'Are you sure you want to revert changes? All flow configuration changes will be reverted to the last version.',
-                noText: 'Cancel',
-                yesText: 'Revert',
-                yesHandler: function () {
-                    getVersionControlInformation(processGroupId).done(function (response) {
-                        if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
-                            var revertTimer = null;
-                            var revertRequest = null;
-                            var cancelled = false;
-
-                            // update the button model of the revert status dialog
-                            $('#change-version-status-dialog').modal('setButtonModel', [{
-                                buttonText: 'Stop',
-                                color: {
-                                    base: '#728E9B',
-                                    hover: '#004849',
-                                    text: '#ffffff'
-                                },
-                                handler: {
-                                    click: function () {
-                                        cancelled = true;
-
-                                        $('#change-version-status-dialog').modal('setButtonModel', []);
-
-                                        // we are waiting for the next poll attempt
-                                        if (revertTimer !== null) {
-                                            // cancel it
-                                            clearTimeout(revertTimer);
-
-                                            // cancel the revert request
-                                            completeRevertRequest();
-                                        }
-                                    }
-                                }
-                            }]);
-
-                            // hide the import dialog immediately
-                            $('#import-flow-version-dialog').modal('hide');
-
-                            var submitRevertRequest = function () {
-                                var revertFlowVersionRequest = {
-                                    'processGroupRevision': nfClient.getRevision({
-                                        'revision': {
-                                            'version': response.processGroupRevision.version
-                                        }
-                                    }),
-                                    'versionControlInformation': response.versionControlInformation
-                                };
-
-                                return $.ajax({
-                                    type: 'POST',
-                                    data: JSON.stringify(revertFlowVersionRequest),
-                                    url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
-                                    dataType: 'json',
-                                    contentType: 'application/json'
-                                }).done(function () {
-                                    // initialize the progress bar value
-                                    updateProgress(0);
-
-                                    // show the progress dialog
-                                    $('#change-version-status-dialog').modal('show');
-                                }).fail(nfErrorHandler.handleAjaxError);
-                            };
-
-                            var pollRevertRequest = function () {
-                                getRevertRequest().done(processRevertResponse);
-                            };
-
-                            var getRevertRequest = function () {
-                                return $.ajax({
-                                    type: 'GET',
-                                    url: revertRequest.uri,
-                                    dataType: 'json'
-                                }).fail(completeRevertRequest).fail(nfErrorHandler.handleAjaxError);
-                            };
-
-                            var completeRevertRequest = function () {
-                                if (cancelled === true) {
-                                    // update the message to indicate successful completion
-                                    $('#change-version-status-message').text('The revert request has been cancelled.');
-
-                                    // update the button model
-                                    $('#change-version-status-dialog').modal('setButtonModel', [{
-                                        buttonText: 'Close',
-                                        color: {
-                                            base: '#728E9B',
-                                            hover: '#004849',
-                                            text: '#ffffff'
-                                        },
-                                        handler: {
-                                            click: function () {
-                                                $(this).modal('hide');
-                                            }
-                                        }
-                                    }]);
-                                }
-
-                                if (nfCommon.isDefinedAndNotNull(revertRequest)) {
-                                    $.ajax({
-                                        type: 'DELETE',
-                                        url: revertRequest.uri,
-                                        dataType: 'json'
-                                    }).done(function (response) {
-                                        revertRequest = response.request;
-
-                                        // update the component that was changing
-                                        updateProcessGroup(processGroupId);
-
-                                        if (nfCommon.isDefinedAndNotNull(revertRequest.failureReason)) {
-                                            // hide the progress dialog
-                                            $('#change-version-status-dialog').modal('hide');
-
-                                            nfDialog.showOkDialog({
-                                                headerText: 'Revert Local Changes',
-                                                dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
-                                            });
-                                        } else {
-                                            // update the percent complete
-                                            updateProgress(revertRequest.percentCompleted);
-
-                                            // update the message to indicate successful completion
-                                            $('#change-version-status-message').text('This Process Group version has changed.');
-
-                                            // update the button model
-                                            $('#change-version-status-dialog').modal('setButtonModel', [{
-                                                buttonText: 'Close',
-                                                color: {
-                                                    base: '#728E9B',
-                                                    hover: '#004849',
-                                                    text: '#ffffff'
-                                                },
-                                                handler: {
-                                                    click: function () {
-                                                        $(this).modal('hide');
-                                                    }
-                                                }
-                                            }]);
-                                        }
-                                    });
-                                }
-                            };
-
-                            var processRevertResponse = function (response) {
-                                revertRequest = response.request;
-
-                                if (revertRequest.complete === true || cancelled === true) {
-                                    completeRevertRequest();
-                                } else {
-                                    // update the percent complete
-                                    updateProgress(revertRequest.percentCompleted);
-
-                                    // update the status of the revert request
-                                    $('#change-version-status-message').text(revertRequest.state);
-
-                                    revertTimer = setTimeout(function () {
-                                        // clear the timer since we've been invoked
-                                        revertTimer = null;
-
-                                        // poll revert request
-                                        pollRevertRequest();
-                                    }, 2000);
-                                }
-                            };
+            loadLocalChanges(processGroupId, $('#revert-local-changes-table'), $('#total-revert-local-changes-entries')).done(function () {
+                $('#revert-local-changes-process-group-id').text(processGroupId);
+                $('#revert-local-changes-dialog').modal('show');
+            });
+        },
 
-                            submitRevertRequest().done(processRevertResponse);
-                        } else {
-                            nfDialog.showOkDialog({
-                                headerText: 'Revert Changes',
-                                dialogContent: 'This Process Group is not currently under version control.'
-                            });
-                        }
-                    }).fail(nfErrorHandler.handleAjaxError);
-                }
+        /**
+         * Shows local changes for the specified process group.
+         *
+         * @param processGroupId
+         */
+        showLocalChanges: function (processGroupId) {
+            loadLocalChanges(processGroupId, $('#show-local-changes-table'), $('#total-show-local-changes-entries')).done(function () {
+                $('#show-local-changes-dialog').modal('show');
             });
         },
 
@@ -1407,8 +1801,8 @@
         stopVersionControl: function (processGroupId) {
             // prompt the user before disconnecting
             nfDialog.showYesNoDialog({
-                headerText: 'Disconnect',
-                dialogContent: 'Are you sure you want to disconnect?',
+                headerText: 'Stop Version Control',
+                dialogContent: 'Are you sure you want to stop version control?',
                 noText: 'Cancel',
                 yesText: 'Disconnect',
                 yesHandler: function () {
@@ -1434,7 +1828,7 @@
 
                                 nfDialog.showOkDialog({
                                     headerText: 'Disconnect',
-                                    dialogContent: 'This Process Group has been disconnected.'
+                                    dialogContent: 'This Process Group is no longer under version control.'
                                 });
                             }).fail(nfErrorHandler.handleAjaxError);
                         } else {


[35/50] nifi git commit: NIFI-4436: Bug fixes; ensure correct Exception types are thrown

Posted by bb...@apache.org.
NIFI-4436: Bug fixes; ensure correct Exception types are thrown

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/181d6809
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/181d6809
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/181d6809

Branch: refs/heads/master
Commit: 181d6809c126da862417e79fb5d794ed5f8eefac
Parents: 1266235
Author: Mark Payne <ma...@hotmail.com>
Authored: Mon Dec 11 15:36:56 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:55 2018 -0500

----------------------------------------------------------------------
 .../manager/ProcessGroupEntityMerger.java       |  3 +
 .../org/apache/nifi/groups/ProcessGroup.java    |  2 +-
 .../apache/nifi/registry/flow/FlowRegistry.java |  1 -
 .../apache/nifi/controller/FlowController.java  | 14 ++-
 .../nifi/groups/StandardProcessGroup.java       | 50 +++++++---
 .../flow/StandardFlowRegistryClient.java        | 11 ++-
 .../java/org/apache/nifi/util/SnippetUtils.java | 99 ++++++++++++++++++++
 .../StandardFlowSynchronizerSpec.groovy         |  2 +
 .../service/mock/MockProcessGroup.java          |  2 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  | 22 ++---
 .../nifi/web/StandardNiFiServiceFacade.java     | 72 +++++++++-----
 .../nifi/web/api/ProcessGroupResource.java      |  9 +-
 .../apache/nifi/web/api/VersionsResource.java   |  4 +-
 .../dao/impl/StandardControllerServiceDAO.java  | 30 +++---
 .../web/dao/impl/StandardProcessGroupDAO.java   |  2 +-
 .../org/apache/nifi/web/util/SnippetUtils.java  |  1 -
 16 files changed, 246 insertions(+), 78 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
index d2eb749..d74fdeb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
@@ -48,6 +48,9 @@ public class ProcessGroupEntityMerger implements ComponentEntityMerger<ProcessGr
     private void mergeVersionControlInformation(ProcessGroupEntity targetGroup, ProcessGroupEntity toMerge) {
         final ProcessGroupDTO targetGroupDto = targetGroup.getComponent();
         final ProcessGroupDTO toMergeGroupDto = toMerge.getComponent();
+        if (targetGroupDto == null || toMergeGroupDto == null) {
+            return;
+        }
 
         final VersionControlInformationDTO targetVersionControl = targetGroupDto.getVersionControlInformation();
         final VersionControlInformationDTO toMergeVersionControl = toMergeGroupDto.getVersionControlInformation();

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index 17131dd..3f32580 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -965,7 +965,7 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
     /**
      * Disconnects this Process Group from version control. If not currently under version control, this method does nothing.
      */
-    void disconnectVersionControl();
+    void disconnectVersionControl(boolean removeVersionedComponentIds);
 
     /**
      * Synchronizes the Process Group with the given Flow Registry, determining whether or not the local flow

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
index ae43bb5..b883274 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -212,6 +212,5 @@ public interface FlowRegistry {
      * @throws IOException if unable to communicate with the Flow Registry
      * @throws NiFiRegistryException if unable to find a flow with the given bucket ID and flow ID
      */
-    // TODO: Do we still need this?
     VersionedFlow getVersionedFlow(String bucketId, String flowId) throws IOException, NiFiRegistryException;
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 158aaa2..f7c2545 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -234,6 +234,7 @@ import org.apache.nifi.util.ComponentIdGenerator;
 import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.ReflectionUtils;
+import org.apache.nifi.util.SnippetUtils;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.api.dto.BatchSettingsDTO;
 import org.apache.nifi.web.api.dto.BundleDTO;
@@ -2372,26 +2373,29 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
      * </p>
      *
      * @param group group
-     * @param templateContents contents
+     * @param snippetContents contents
      */
-    private void validateSnippetContents(final ProcessGroup group, final FlowSnippetDTO templateContents) {
+    private void validateSnippetContents(final ProcessGroup group, final FlowSnippetDTO snippetContents) {
         // validate the names of Input Ports
-        for (final PortDTO port : templateContents.getInputPorts()) {
+        for (final PortDTO port : snippetContents.getInputPorts()) {
             if (group.getInputPortByName(port.getName()) != null) {
                 throw new IllegalStateException("One or more of the proposed Port names is not available in the process group");
             }
         }
 
         // validate the names of Output Ports
-        for (final PortDTO port : templateContents.getOutputPorts()) {
+        for (final PortDTO port : snippetContents.getOutputPorts()) {
             if (group.getOutputPortByName(port.getName()) != null) {
                 throw new IllegalStateException("One or more of the proposed Port names is not available in the process group");
             }
         }
 
-        verifyComponentTypesInSnippet(templateContents);
+        verifyComponentTypesInSnippet(snippetContents);
+
+        SnippetUtils.verifyNoVersionControlConflicts(snippetContents, group);
     }
 
+
     /**
      * Recursively finds all ConnectionDTO's
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 7d184df..5d5d0f4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -112,6 +112,7 @@ import org.apache.nifi.scheduling.ExecutionNode;
 import org.apache.nifi.scheduling.SchedulingStrategy;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.ReflectionUtils;
+import org.apache.nifi.util.SnippetUtils;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.slf4j.Logger;
@@ -2294,6 +2295,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         try {
             verifyContents(snippet);
             verifyDestinationNotInSnippet(snippet, destination);
+            SnippetUtils.verifyNoVersionControlConflicts(snippet, this, destination);
 
             if (!isDisconnected(snippet)) {
                 throw new IllegalStateException("One or more components within the snippet is connected to a component outside of the snippet. Only a disconnected snippet may be moved.");
@@ -3087,13 +3089,15 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     @Override
-    public void disconnectVersionControl() {
+    public void disconnectVersionControl(final boolean removeVersionedComponentIds) {
         writeLock.lock();
         try {
             this.versionControlInfo.set(null);
 
-            // remove version component ids from each component (until another versioned PG is encountered)
-            applyVersionedComponentIds(this, id -> null);
+            if (removeVersionedComponentIds) {
+                // remove version component ids from each component (until another versioned PG is encountered)
+                applyVersionedComponentIds(this, id -> null);
+            }
         } finally {
             writeLock.unlock();
         }
@@ -3278,7 +3282,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final Set<String> knownVariables = getKnownVariableNames();
             updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings, updateDescendantVersionedFlows, knownVariables);
         } catch (final ProcessorInstantiationException pie) {
-            throw new RuntimeException(pie);
+            throw new IllegalStateException("Failed to update flow", pie);
         } finally {
             writeLock.unlock();
         }
@@ -3366,7 +3370,9 @@ public final class StandardProcessGroup implements ProcessGroup {
         group.setVariables(updatedVariableMap);
 
         final VersionedFlowCoordinates remoteCoordinates = proposed.getVersionedFlowCoordinates();
-        if (remoteCoordinates != null) {
+        if (remoteCoordinates == null) {
+            group.disconnectVersionControl(false);
+        } else {
             final String registryId = flowController.getFlowRegistryClient().getFlowRegistryId(remoteCoordinates.getRegistryUrl());
             final String bucketId = remoteCoordinates.getBucketId();
             final String flowId = remoteCoordinates.getFlowId();
@@ -3681,8 +3687,6 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private String generateUuid(final String propposedId, final String destinationGroupId, final String seed) {
-        // TODO: I think we can get rid of all of those LinkedHashSet's now in the VersionedProcessGroup because
-        /// the UUID is properly keyed off of the ID of the component in the VersionedProcessGroup.
         long msb = UUID.nameUUIDFromBytes((propposedId + destinationGroupId).getBytes(StandardCharsets.UTF_8)).getMostSignificantBits();
 
         UUID uuid;
@@ -3733,7 +3737,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 try {
                     return flowController.createPrioritizer(prioritizerName);
                 } catch (final Exception e) {
-                    throw new RuntimeException("Failed to create Prioritizer of type " + prioritizerName + " for Connection with ID " + connection.getIdentifier());
+                    throw new IllegalStateException("Failed to create Prioritizer of type " + prioritizerName + " for Connection with ID " + connection.getIdentifier());
                 }
             })
             .collect(Collectors.toList());
@@ -4016,7 +4020,14 @@ public final class StandardProcessGroup implements ProcessGroup {
                     // The value given is instead the Versioned Component ID of the Controller Service. We want to resolve this
                     // to the instance ID of the Controller Service.
                     final String serviceVersionedComponentId = entry.getValue();
-                    final String instanceId = getServiceInstanceId(serviceVersionedComponentId, group);
+                    String instanceId = getServiceInstanceId(serviceVersionedComponentId, group);
+                    if (instanceId == null) {
+                        // We didn't find the instance ID based on the Versioned Component ID. So we want to just
+                        // leave the value set to whatever it currently is, if it's currently set.
+                        final PropertyDescriptor propertyDescriptor = new PropertyDescriptor.Builder().name(entry.getKey()).build();
+                        instanceId = currentProperties.get(propertyDescriptor);
+                    }
+
                     value = instanceId == null ? serviceVersionedComponentId : instanceId;
                 } else {
                     value = entry.getValue();
@@ -4169,15 +4180,22 @@ public final class StandardProcessGroup implements ProcessGroup {
                             + " reverted to its original form before changing the version.");
                     }
                 }
+
+                verifyNoDescendantsWithLocalModifications("be updated");
             }
 
             final VersionedProcessGroup flowContents = updatedFlow.getFlowContents();
             if (verifyConnectionRemoval) {
                 // Determine which Connections have been removed.
                 final Map<String, Connection> removedConnectionByVersionedId = new HashMap<>();
+
+                // Populate the 'removedConnectionByVersionId' map with all Connections. We key off of the connection's VersionedComponentID
+                // if it is populated. Otherwise, we key off of its actual ID. We do this because it allows us to then remove from this Map
+                // any connection that does exist in the proposed flow. This results in us having a Map whose values are those Connections
+                // that were removed. We can then check for any connections that have data in them. If any Connection is to be removed but
+                // has data, then we should throw an IllegalStateException.
                 findAllConnections().stream()
-                    .filter(conn -> conn.getVersionedComponentId().isPresent())
-                    .forEach(conn -> removedConnectionByVersionedId.put(conn.getVersionedComponentId().get(), conn));
+                    .forEach(conn -> removedConnectionByVersionedId.put(conn.getVersionedComponentId().orElse(conn.getIdentifier()), conn));
 
                 final Set<String> proposedFlowConnectionIds = new HashSet<>();
                 findAllConnectionIds(flowContents, proposedFlowConnectionIds);
@@ -4252,8 +4270,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 if (!proposedProcessGroups.containsKey(versionedId)) {
                     // Process Group was removed.
                     throw new IllegalStateException(this + " cannot be updated to the proposed version of the flow because the child " + childGroup
-                        + " that exists locally has one or more Templates, and the proposed flow does not contain this Process Group. "
-                        + "A Process Group cannot be deleted while it contains Templates. Please remove the Templates before attempting to chnage the version of the flow.");
+                        + " that exists locally has one or more Templates, and the proposed flow does not contain these templates. "
+                        + "A Process Group cannot be deleted while it contains Templates. Please remove the Templates before attempting to change the version of the flow.");
                 }
             }
 
@@ -4430,6 +4448,12 @@ public final class StandardProcessGroup implements ProcessGroup {
                         + "has local modifications. Each descendant Process Group that is under Version Control must first be reverted or have its changes pushed to the Flow Registry before "
                         + "this action can be performed on the parent Process Group.");
                 }
+
+                if (flowState == VersionedFlowState.SYNC_FAILURE) {
+                    throw new IllegalStateException("Process Group cannot " + action + " because it contains a child or descendant Process Group that is under Version Control and "
+                        + "is not synchronized with the Flow Registry. Each descendant Process Group must first be synchronized with the Flow Registry before this action can be "
+                        + "performed on the parent Process Group. NiFi will continue to attempt to communicate with the Flow Registry periodically in the background.");
+                }
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
index 8a2447d..4f98a2b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
@@ -43,6 +43,13 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
 
     @Override
     public void addFlowRegistry(final FlowRegistry registry) {
+        final boolean duplicateName = registryById.values().stream()
+            .anyMatch(reg -> reg.getName().equals(registry.getName()));
+
+        if (duplicateName) {
+            throw new IllegalStateException("Cannot add Flow Registry because a Flow Registry already exists with the name " + registry.getName());
+        }
+
         final FlowRegistry existing = registryById.putIfAbsent(registry.getIdentifier(), registry);
         if (existing != null) {
             throw new IllegalStateException("Cannot add Flow Registry " + registry + " because a Flow Registry already exists with the ID " + registry.getIdentifier());
@@ -58,7 +65,7 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
         if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
             final SSLContext sslContext = SslContextFactory.createSslContext(nifiProperties, false);
             if (sslContext == null && uriScheme.equalsIgnoreCase("https")) {
-                throw new RuntimeException("Failed to create Flow Registry for URI " + registryUrl
+                throw new IllegalStateException("Failed to create Flow Registry for URI " + registryUrl
                     + " because this NiFi is not configured with a Keystore/Truststore, so it is not capable of communicating with a secure Registry. "
                     + "Please populate NiFi's Keystore/Truststore properties or connect to a NiFi Registry over http instead of https.");
             }
@@ -68,7 +75,7 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
         } else if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
             final SSLContext sslContext = SslContextFactory.createSslContext(nifiProperties, false);
             if (sslContext == null && uriScheme.equalsIgnoreCase("https")) {
-                throw new RuntimeException("Failed to create Flow Registry for URI " + registryUrl
+                throw new IllegalStateException("Failed to create Flow Registry for URI " + registryUrl
                     + " because this NiFi is not configured with a Keystore/Truststore, so it is not capable of communicating with a secure Registry. "
                     + "Please populate NiFi's Keystore/Truststore properties or connect to a NiFi Registry over http instead of https.");
             }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/SnippetUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/SnippetUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/SnippetUtils.java
index b482169..9c04559 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/SnippetUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/SnippetUtils.java
@@ -16,11 +16,15 @@
  */
 package org.apache.nifi.util;
 
+import org.apache.nifi.controller.Snippet;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.web.api.dto.ComponentDTO;
 import org.apache.nifi.web.api.dto.ConnectionDTO;
 import org.apache.nifi.web.api.dto.FlowSnippetDTO;
 import org.apache.nifi.web.api.dto.PositionDTO;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -307,4 +311,99 @@ public final class SnippetUtils {
             connection.setBends(bends);
         }
     }
+
+    public static void verifyNoVersionControlConflicts(final Snippet snippet, final ProcessGroup parentGroup, final ProcessGroup destination) {
+        if (snippet == null) {
+            return;
+        }
+        if (snippet.getProcessGroups() == null) {
+            return;
+        }
+
+        final List<VersionControlInformation> vcis = new ArrayList<>();
+        for (final String groupId : snippet.getProcessGroups().keySet()) {
+            final ProcessGroup group = parentGroup.getProcessGroup(groupId);
+            if (group != null) {
+                findAllVersionControlInfo(group, vcis);
+            }
+        }
+
+        verifyNoDuplicateVersionControlInfo(destination, vcis);
+    }
+
+    public static void verifyNoVersionControlConflicts(final FlowSnippetDTO snippetContents, final ProcessGroup destination) {
+        final List<VersionControlInformationDTO> vcis = new ArrayList<>();
+        for (final ProcessGroupDTO childGroup : snippetContents.getProcessGroups()) {
+            findAllVersionControlInfo(childGroup, vcis);
+        }
+
+        verifyNoDuplicateVersionControlInfoDtos(destination, vcis);
+    }
+
+    private static void verifyNoDuplicateVersionControlInfoDtos(final ProcessGroup group, final Collection<VersionControlInformationDTO> snippetVcis) {
+        final VersionControlInformation vci = group.getVersionControlInformation();
+        if (vci != null) {
+            for (final VersionControlInformationDTO snippetVci : snippetVcis) {
+                if (vci.getBucketIdentifier().equals(snippetVci.getBucketId()) && vci.getFlowIdentifier().equals(snippetVci.getFlowId())) {
+                    throw new IllegalArgumentException("Cannot place the given Process Group into the desired destination because the destination group or one of its ancestor groups is "
+                        + "under Version Control and one of the selected Process Groups is also under Version Control with the same Flow. A Process Group that is under Version Control "
+                        + "cannot contain a child Process Group that points to the same Versioned Flow.");
+                }
+            }
+        }
+
+        final ProcessGroup parent = group.getParent();
+        if (parent != null) {
+            verifyNoDuplicateVersionControlInfoDtos(parent, snippetVcis);
+        }
+    }
+
+    private static void verifyNoDuplicateVersionControlInfo(final ProcessGroup group, final Collection<VersionControlInformation> snippetVcis) {
+        final VersionControlInformation vci = group.getVersionControlInformation();
+        if (vci != null) {
+            for (final VersionControlInformation snippetVci : snippetVcis) {
+                if (vci.getBucketIdentifier().equals(snippetVci.getBucketIdentifier()) && vci.getFlowIdentifier().equals(snippetVci.getFlowIdentifier())) {
+                    throw new IllegalArgumentException("Cannot place the given Process Group into the desired destination because the destination group or one of its ancestor groups is "
+                        + "under Version Control and one of the selected Process Groups is also under Version Control with the same Flow. A Process Group that is under Version Control "
+                        + "cannot contain a child Process Group that points to the same Versioned Flow.");
+                }
+            }
+        }
+
+        final ProcessGroup parent = group.getParent();
+        if (parent != null) {
+            verifyNoDuplicateVersionControlInfo(parent, snippetVcis);
+        }
+    }
+
+
+    private static void findAllVersionControlInfo(final ProcessGroupDTO dto, final List<VersionControlInformationDTO> found) {
+        final VersionControlInformationDTO vci = dto.getVersionControlInformation();
+        if (vci != null) {
+            found.add(vci);
+        }
+
+        final FlowSnippetDTO contents = dto.getContents();
+        if (contents != null) {
+            for (final ProcessGroupDTO child : contents.getProcessGroups()) {
+                findAllVersionControlInfo(child, found);
+            }
+        }
+    }
+
+    private static void findAllVersionControlInfo(final ProcessGroup group, final List<VersionControlInformation> found) {
+        if (group == null) {
+            return;
+        }
+
+        final VersionControlInformation vci = group.getVersionControlInformation();
+        if (vci != null) {
+            found.add(vci);
+        }
+
+        for (final ProcessGroup childGroup : group.findAllProcessGroups()) {
+            findAllVersionControlInfo(childGroup, found);
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/StandardFlowSynchronizerSpec.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/StandardFlowSynchronizerSpec.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/StandardFlowSynchronizerSpec.groovy
index 7483228..897d77e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/StandardFlowSynchronizerSpec.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/controller/StandardFlowSynchronizerSpec.groovy
@@ -127,6 +127,8 @@ class StandardFlowSynchronizerSpec extends Specification {
                     }
                 }
             }
+			_ * processGroup.findAllRemoteProcessGroups() >> []
+            
             positionableMocksById.put(pgId, processGroup)
             return processGroup
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
index d006cff..95e2d6a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
@@ -667,7 +667,7 @@ public class MockProcessGroup implements ProcessGroup {
     }
 
     @Override
-    public void disconnectVersionControl() {
+    public void disconnectVersionControl(final boolean removeVersionedComponentIds) {
         this.versionControlInfo = null;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 514cd18..6c20eac 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -23,7 +23,6 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.repository.claim.ContentDirection;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
-import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
@@ -117,7 +116,6 @@ import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 
-import java.io.IOException;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -423,11 +421,12 @@ public interface NiFiServiceFacade {
      * with the given id
      *
      * @param versionControlInfo the information about the versioned flow
+     * @param versionedProcessGroup the contents to be imported
      * @param groupId the ID of the Process Group where the flow should be instantiated
      *
      * @throws IllegalStateException if the flow cannot be imported into the specified group
      */
-    void verifyImportProcessGroup(VersionControlInformationDTO versionControlInfo, String groupId);
+    void verifyImportProcessGroup(VersionControlInformationDTO versionControlInfo, final VersionedProcessGroup versionedProcessGroup, String groupId);
 
     /**
      * Creates a new Template based off the specified snippet.
@@ -1295,7 +1294,7 @@ public interface NiFiServiceFacade {
      *         was last synchronized with the Flow Registry
      * @throws IllegalStateException if the Process Group with the given ID is not under version control
      */
-    FlowComparisonEntity getLocalModifications(String processGroupId) throws IOException, NiFiRegistryException;
+    FlowComparisonEntity getLocalModifications(String processGroupId);
 
     /**
      * Returns the Version Control information for the Process Group with the given ID
@@ -1314,9 +1313,9 @@ public interface NiFiServiceFacade {
      * @param flow the flow to add to the registry
      * @return a VersionedFlow that is fully populated, including identifiers
      *
-     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws NiFiCoreException if unable to register flow
      */
-    VersionedFlow registerVersionedFlow(String registryId, VersionedFlow flow) throws IOException, NiFiRegistryException;
+    VersionedFlow registerVersionedFlow(String registryId, VersionedFlow flow);
 
     /**
      * Creates a snapshot of the Process Group with the given identifier, then creates a new Flow entity in the NiFi Registry
@@ -1337,7 +1336,7 @@ public interface NiFiServiceFacade {
      * @param flowId the ID of the flow
      * @return the VersionedFlow that was deleted
      */
-    VersionedFlow deleteVersionedFlow(String registryId, String bucketId, String flowId) throws IOException, NiFiRegistryException;
+    VersionedFlow deleteVersionedFlow(String registryId, String bucketId, String flowId);
 
     /**
      * Adds the given snapshot to the already existing Versioned Flow, which resides in the given Flow Registry with the given id
@@ -1349,10 +1348,9 @@ public interface NiFiServiceFacade {
      * @param expectedVersion the version to save the flow as
      * @return the snapshot that represents what was stored in the registry
      *
-     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws NiFiCoreException if unable to register the snapshot with the flow registry
      */
-    VersionedFlowSnapshot registerVersionedFlowSnapshot(String registryId, VersionedFlow flow, VersionedProcessGroup snapshot, String comments, int expectedVersion)
-        throws IOException, NiFiRegistryException;
+    VersionedFlowSnapshot registerVersionedFlowSnapshot(String registryId, VersionedFlow flow, VersionedProcessGroup snapshot, String comments, int expectedVersion);
 
     /**
      * Updates the Version Control Information on the Process Group with the given ID
@@ -1386,7 +1384,7 @@ public interface NiFiServiceFacade {
      *
      * @throws ResourceNotFoundException if the Versioned Flow Snapshot could not be found
      */
-    VersionedFlowSnapshot getVersionedFlowSnapshot(VersionControlInformationDTO versionControlInfo, boolean fetchRemoteFlows) throws IOException;
+    VersionedFlowSnapshot getVersionedFlowSnapshot(VersionControlInformationDTO versionControlInfo, boolean fetchRemoteFlows);
 
     /**
      * Returns the name of the Flow Registry that is registered with the given ID. If no Flow Registry exists with the given ID, will return
@@ -1406,7 +1404,7 @@ public interface NiFiServiceFacade {
      * @param user the user making the request
      * @return the set of all components that would be affected by updating the Process Group
      */
-    Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(String processGroupId, VersionedFlowSnapshot updatedSnapshot, NiFiUser user) throws IOException;
+    Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(String processGroupId, VersionedFlowSnapshot updatedSnapshot, NiFiUser user);
 
     /**
      * Verifies that the Process Group with the given identifier can be updated to the proposed flow

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index a2d6e41..11bc39d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -95,6 +95,7 @@ import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedFlowState;
@@ -1866,20 +1867,21 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public void verifyImportProcessGroup(final VersionControlInformationDTO versionControlInfo, final String groupId) {
+    public void verifyImportProcessGroup(final VersionControlInformationDTO versionControlInfo, final VersionedProcessGroup contents, final String groupId) {
         final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
-        verifyImportProcessGroup(versionControlInfo, group);
+        verifyImportProcessGroup(versionControlInfo, contents, group);
     }
 
-    private void verifyImportProcessGroup(final VersionControlInformationDTO vciDto, final ProcessGroup group) {
+    private void verifyImportProcessGroup(final VersionControlInformationDTO vciDto, final VersionedProcessGroup contents, final ProcessGroup group) {
         if (group == null) {
             return;
         }
 
         final VersionControlInformation vci = group.getVersionControlInformation();
         if (vci != null) {
-            if (Objects.equals(vciDto.getRegistryId(), vci.getRegistryIdentifier())
-                && Objects.equals(vciDto.getBucketId(), vci.getBucketIdentifier())
+            // Note that we do not compare the Registry ID here because there could be two registry clients
+            // that point to the same server (one could point to localhost while another points to 127.0.0.1, for instance)..
+            if (Objects.equals(vciDto.getBucketId(), vci.getBucketIdentifier())
                 && Objects.equals(vciDto.getFlowId(), vci.getFlowIdentifier())) {
 
                 throw new IllegalStateException("Cannot import the specified Versioned Flow into the Process Group because doing so would cause a recursive dataflow. "
@@ -1887,7 +1889,20 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             }
         }
 
-        verifyImportProcessGroup(vciDto, group.getParent());
+        final Set<VersionedProcessGroup> childGroups = contents.getProcessGroups();
+        if (childGroups != null) {
+            for (final VersionedProcessGroup childGroup : childGroups) {
+                final VersionedFlowCoordinates childCoordinates = childGroup.getVersionedFlowCoordinates();
+                if (childCoordinates != null) {
+                    final VersionControlInformationDTO childVci = new VersionControlInformationDTO();
+                    childVci.setBucketId(childCoordinates.getBucketId());
+                    childVci.setFlowId(childCoordinates.getFlowId());
+                    verifyImportProcessGroup(childVci, childGroup, group);
+                }
+            }
+        }
+
+        verifyImportProcessGroup(vciDto, contents, group.getParent());
     }
 
     @Override
@@ -3447,8 +3462,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         entity.setPoliciesPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getPolicies()));
         entity.setSystemPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getSystem()));
         entity.setRestrictedComponentsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getRestrictedComponents()));
-
-        // TODO - update to be user specific
         entity.setCanVersionFlows(CollectionUtils.isNotEmpty(flowRegistryClient.getRegistryIdentifiers()));
 
         return entity;
@@ -3722,13 +3735,17 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public VersionedFlow deleteVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
+    public VersionedFlow deleteVersionedFlow(final String registryId, final String bucketId, final String flowId) {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
         if (registry == null) {
             throw new IllegalArgumentException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.deleteVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser());
+        try {
+            return registry.deleteVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser());
+        } catch (final IOException | NiFiRegistryException e) {
+            throw new NiFiCoreException("Failed to remove flow from Flow Registry due to " + e.getMessage(), e);
+        }
     }
 
     @Override
@@ -3752,7 +3769,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public FlowComparisonEntity getLocalModifications(final String processGroupId) throws IOException, NiFiRegistryException {
+    public FlowComparisonEntity getLocalModifications(final String processGroupId) {
         final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId);
         final VersionControlInformation versionControlInfo = processGroup.getVersionControlInformation();
         if (versionControlInfo == null) {
@@ -3765,11 +3782,16 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                 + " but cannot find a Flow Registry with that identifier");
         }
 
-        final VersionedFlowSnapshot versionedFlowSnapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketIdentifier(),
-            versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), false, NiFiUserUtils.getNiFiUser());
+        final VersionedFlowSnapshot versionedFlowSnapshot;
+        try {
+            versionedFlowSnapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketIdentifier(),
+                versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), true, NiFiUserUtils.getNiFiUser());
+        } catch (final IOException | NiFiRegistryException e) {
+            throw new NiFiCoreException("Failed to retrieve flow with Flow Registry in order to calculate local differences due to " + e.getMessage(), e);
+        }
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, false);
+        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, true);
         final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents();
 
         final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localGroup);
@@ -3802,13 +3824,17 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) throws IOException, NiFiRegistryException {
+    public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
         if (registry == null) {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.registerVersionedFlow(flow, NiFiUserUtils.getNiFiUser());
+        try {
+            return registry.registerVersionedFlow(flow, NiFiUserUtils.getNiFiUser());
+        } catch (final IOException | NiFiRegistryException e) {
+            throw new NiFiCoreException("Failed to register flow with Flow Registry due to " + e.getMessage(), e);
+        }
     }
 
     private VersionedFlow getVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
@@ -3822,13 +3848,17 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public VersionedFlowSnapshot registerVersionedFlowSnapshot(final String registryId, final VersionedFlow flow,
-        final VersionedProcessGroup snapshot, final String comments, final int expectedVersion) throws IOException, NiFiRegistryException {
+        final VersionedProcessGroup snapshot, final String comments, final int expectedVersion) {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
         if (registry == null) {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.registerVersionedFlowSnapshot(flow, snapshot, comments, expectedVersion, NiFiUserUtils.getNiFiUser());
+        try {
+            return registry.registerVersionedFlowSnapshot(flow, snapshot, comments, expectedVersion, NiFiUserUtils.getNiFiUser());
+        } catch (final IOException | NiFiRegistryException e) {
+            throw new NiFiCoreException("Failed to register flow with Flow Registry due to " + e.getMessage(), e);
+        }
     }
 
     @Override
@@ -3881,7 +3911,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(final String processGroupId, final VersionedFlowSnapshot updatedSnapshot, final NiFiUser user) throws IOException {
+    public Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(final String processGroupId, final VersionedFlowSnapshot updatedSnapshot, final NiFiUser user) {
         final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
@@ -4057,7 +4087,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public VersionedFlowSnapshot getVersionedFlowSnapshot(final VersionControlInformationDTO versionControlInfo, final boolean fetchRemoteFlows) throws IOException {
+    public VersionedFlowSnapshot getVersionedFlowSnapshot(final VersionControlInformationDTO versionControlInfo, final boolean fetchRemoteFlows) {
         final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(versionControlInfo.getRegistryId());
         if (flowRegistry == null) {
             throw new ResourceNotFoundException("Could not find any Flow Registry registered with identifier " + versionControlInfo.getRegistryId());
@@ -4066,7 +4096,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final VersionedFlowSnapshot snapshot;
         try {
             snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion(), fetchRemoteFlows, NiFiUserUtils.getNiFiUser());
-        } catch (final NiFiRegistryException e) {
+        } catch (final NiFiRegistryException | IOException e) {
             throw new IllegalArgumentException("The Flow Registry with ID " + versionControlInfo.getRegistryId() + " reports that no Flow exists with Bucket "
                 + versionControlInfo.getBucketId() + ", Flow " + versionControlInfo.getFlowId() + ", Version " + versionControlInfo.getVersion());
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index b3ccefb..a2c16ed 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -1644,10 +1644,6 @@ public class ProcessGroupResource extends ApplicationResource {
         // Step 6: Replicate the request or call serviceFacade.updateProcessGroup
 
         final VersionControlInformationDTO versionControlInfo = requestProcessGroupEntity.getComponent().getVersionControlInformation();
-        if (versionControlInfo != null) {
-            serviceFacade.verifyImportProcessGroup(versionControlInfo, groupId);
-        }
-
         if (versionControlInfo != null && requestProcessGroupEntity.getVersionedFlowSnapshot() == null) {
             // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
             // Step 2: Retrieve flow from Flow Registry
@@ -1670,6 +1666,11 @@ public class ProcessGroupResource extends ApplicationResource {
             requestProcessGroupEntity.setVersionedFlowSnapshot(flowSnapshot);
         }
 
+        if (versionControlInfo != null) {
+            final VersionedFlowSnapshot flowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
+            serviceFacade.verifyImportProcessGroup(versionControlInfo, flowSnapshot.getFlowContents(), groupId);
+        }
+
         // Step 6: Replicate the request or call serviceFacade.updateProcessGroup
         if (isReplicateRequest()) {
             return replicate(HttpMethod.POST, requestProcessGroupEntity);

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 3090c6e..1d4cd88 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -1110,7 +1110,7 @@ public class VersionsResource extends ApplicationResource {
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
-                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Stopping Processors");
+                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Stopping Affected Processors");
 
                 // Submit the request to be performed in the background
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
@@ -1275,7 +1275,7 @@ public class VersionsResource extends ApplicationResource {
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
-                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Stopping Processors");
+                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Stopping Affected Processors");
 
                 // Submit the request to be performed in the background
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
index 4d8e984..5622097 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
@@ -172,21 +172,23 @@ public class StandardControllerServiceDAO extends ComponentDAO implements Contro
             }
         }
 
-        controllerService.getProcessGroup().onComponentModified();
-
-        // For any component that references this Controller Service, find the component's Process Group
-        // and notify the Process Group that a component has been modified. This way, we know to re-calculate
-        // whether or not the Process Group has local modifications.
         final ProcessGroup group = controllerService.getProcessGroup();
-        controllerService.getReferences().getReferencingComponents().stream()
-            .map(ConfiguredComponent::getProcessGroupIdentifier)
-            .filter(id -> !id.equals(group.getIdentifier()))
-            .forEach(groupId -> {
-                final ProcessGroup descendant = group.findProcessGroup(groupId);
-                if (descendant != null) {
-                    descendant.onComponentModified();
-                }
-            });
+        if (group != null) {
+            group.onComponentModified();
+
+            // For any component that references this Controller Service, find the component's Process Group
+            // and notify the Process Group that a component has been modified. This way, we know to re-calculate
+            // whether or not the Process Group has local modifications.
+            controllerService.getReferences().getReferencingComponents().stream()
+                .map(ConfiguredComponent::getProcessGroupIdentifier)
+                .filter(id -> !id.equals(group.getIdentifier()))
+                .forEach(groupId -> {
+                    final ProcessGroup descendant = group.findProcessGroup(groupId);
+                    if (descendant != null) {
+                        descendant.onComponentModified();
+                    }
+                });
+        }
 
         return controllerService;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 52a18dc..e7e85af 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -274,7 +274,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     @Override
     public ProcessGroup disconnectVersionControl(final String groupId) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
-        group.disconnectVersionControl();
+        group.disconnectVersionControl(true);
         return group;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/181d6809/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
index 11bd1b8..2dbd1ca 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
@@ -90,7 +90,6 @@ public final class SnippetUtils {
     private DtoFactory dtoFactory;
     private AccessPolicyDAO accessPolicyDAO;
 
-
     /**
      * Populates the specified snippet and returns the details.
      *


[10/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
new file mode 100644
index 0000000..4b87b50
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
@@ -0,0 +1,162 @@
+/*
+ * 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.web.api.concurrent;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.web.ResourceNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AsyncRequestManager<T> implements RequestManager<T> {
+    private static final Logger logger = LoggerFactory.getLogger(AsyncRequestManager.class);
+
+    private final long requestExpirationMillis;
+    private final int maxConcurrentRequests;
+    private final ConcurrentMap<String, AsynchronousWebRequest<T>> requests = new ConcurrentHashMap<>();
+
+    private final ExecutorService threadPool;
+
+
+    public AsyncRequestManager(final int maxConcurrentRequests, final long requestExpirationMillis, final String threadNamePrefix) {
+        this.requestExpirationMillis = requestExpirationMillis;
+        this.maxConcurrentRequests = maxConcurrentRequests;
+
+        this.threadPool = new ThreadPoolExecutor(1, 50, 5L, TimeUnit.SECONDS,
+            new ArrayBlockingQueue<Runnable>(maxConcurrentRequests),
+            new ThreadFactory() {
+                private final AtomicLong counter = new AtomicLong(0L);
+
+                @Override
+                public Thread newThread(final Runnable r) {
+                    final Thread thread = Executors.defaultThreadFactory().newThread(r);
+                    thread.setName(threadNamePrefix + "-" + counter.incrementAndGet());
+                    thread.setDaemon(true);
+                    return thread;
+                }
+            });
+
+    }
+
+    private String getKey(final String type, final String request) {
+        return type + "/" + request;
+    }
+
+    @Override
+    public void submitRequest(final String type, final String requestId, final AsynchronousWebRequest<T> request, final Consumer<AsynchronousWebRequest<T>> task) {
+        Objects.requireNonNull(type);
+        Objects.requireNonNull(requestId);
+        Objects.requireNonNull(request);
+        Objects.requireNonNull(task);
+
+        // before adding to the request map, purge any old requests. Must do this by creating a List of ID's
+        // and then removing those ID's one-at-a-time in order to avoid ConcurrentModificationException.
+        final Date oneMinuteAgo = new Date(System.currentTimeMillis() - requestExpirationMillis);
+        final List<String> completedRequestIds = requests.entrySet().stream()
+            .filter(entry -> entry.getValue().isComplete())
+            .filter(entry -> entry.getValue().getLastUpdated().before(oneMinuteAgo))
+            .map(Map.Entry::getKey)
+            .collect(Collectors.toList());
+
+        completedRequestIds.stream().forEach(id -> requests.remove(id));
+
+        final int requestCount = requests.size();
+        if (requestCount > maxConcurrentRequests) {
+            throw new IllegalStateException("There are already " + requestCount + " update requests for variable registries. "
+                + "Cannot issue any more requests until the older ones are deleted or expire");
+        }
+
+        final String key = getKey(type, requestId);
+        final AsynchronousWebRequest<T> existing = this.requests.putIfAbsent(key, request);
+        if (existing != null) {
+            throw new IllegalArgumentException("A requests already exists with this ID and type");
+        }
+
+        threadPool.submit(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    task.accept(request);
+                } catch (final Exception e) {
+                    logger.error("Failed to perform asynchronous task", e);
+                    request.setFailureReason("Encountered unexpected error when performing asynchronous task: " + e);
+                    request.setLastUpdated(new Date());
+                }
+            }
+        });
+    }
+
+
+    @Override
+    public AsynchronousWebRequest<T> removeRequest(final String type, final String id, final NiFiUser user) {
+        Objects.requireNonNull(type);
+        Objects.requireNonNull(id);
+        Objects.requireNonNull(user);
+
+        final String key = getKey(type, id);
+        final AsynchronousWebRequest<T> request = requests.get(key);
+        if (request == null) {
+            throw new ResourceNotFoundException("Could not find a Request with identifier " + id);
+        }
+
+        if (!request.getUser().equals(user)) {
+            throw new IllegalArgumentException("Only the user that submitted the update request can delete it.");
+        }
+
+        if (!request.isComplete()) {
+            throw new IllegalStateException("Cannot remove the request because it is not yet complete");
+        }
+
+        return requests.remove(key);
+    }
+
+    @Override
+    public AsynchronousWebRequest<T> getRequest(final String type, final String id, final NiFiUser user) {
+        Objects.requireNonNull(type);
+        Objects.requireNonNull(id);
+        Objects.requireNonNull(user);
+
+        final String key = getKey(type, id);
+        final AsynchronousWebRequest<T> request = requests.get(key);
+        if (request == null) {
+            throw new ResourceNotFoundException("Could not find a Request with identifier " + id);
+        }
+
+        if (!request.getUser().equals(user)) {
+            throw new IllegalArgumentException("Only the user that submitted the update request can delete it.");
+        }
+
+        return request;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
new file mode 100644
index 0000000..d09f895
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.web.api.concurrent;
+
+import java.util.Date;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+
+public interface AsynchronousWebRequest<T> {
+
+    /**
+     * @return the ID of the process group that the request is for
+     */
+    String getProcessGroupId();
+
+    /**
+     * @return whether or not this request has completed
+     */
+    boolean isComplete();
+
+    /**
+     * @return the Date at which the status of this request was last updated
+     */
+    Date getLastUpdated();
+
+    /**
+     * Updates the Date at which the status of this request was last updated
+     *
+     * @param date the date at which the status of this request was last updated
+     */
+    void setLastUpdated(Date date);
+
+    /**
+     * @return the user who submitted the request
+     */
+    NiFiUser getUser();
+
+    /**
+     * Indicates that this request has completed, successfully or otherwise
+     *
+     * @param results the results of the request
+     */
+    void markComplete(T results);
+
+    /**
+     * Updates the request to indicate the reason that the request failed
+     *
+     * @param explanation the reason that the request failed
+     */
+    void setFailureReason(String explanation);
+
+    /**
+     * Indicates the reason that the request failed, or <code>null</code> if the request has not failed
+     *
+     * @param explanation the reason that the request failed, or <code>null</code> if the request has not failed
+     */
+    String getFailureReason();
+
+    /**
+     * Returns the results of the request, if it completed successfully, or <code>null</code> if the request either has no completed or failed
+     *
+     * @return the results of the request, if it completed successfully, or <code>null</code> if the request either has no completed or failed
+     */
+    T getResults();
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/RequestManager.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/RequestManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/RequestManager.java
new file mode 100644
index 0000000..580ab47
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/RequestManager.java
@@ -0,0 +1,69 @@
+/*
+ * 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.web.api.concurrent;
+
+import java.util.function.Consumer;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+
+public interface RequestManager<T> {
+
+    /**
+     * Submits a request to be performed in the background
+     *
+     * @param requestType the type of request to submit. This value can be anything and is used along with the id in order to create
+     *            a composite key for the request so that different request types may easily be managed by the RequestManager.
+     * @param id the ID of the request
+     * @param request the request
+     * @param task the task that should be performed in the background
+     *
+     * @throws IllegalArgumentException if a request already exists with the given ID
+     * @throws NullPointerException if any argument is null
+     */
+    void submitRequest(String requestType, String id, AsynchronousWebRequest<T> request, Consumer<AsynchronousWebRequest<T>> task);
+
+    /**
+     * Retrieves the request with the given ID
+     *
+     * @param requestType the type of the request being retrieved
+     * @param id the ID of the request
+     * @param user the user who is retrieving the request
+     * @return the request with the given ID
+     *
+     * @throws ResourceNotFoundException if no request can be found with the given ID
+     * @throws IllegalArgumentException if the user given is not the user that submitted the request
+     * @throws NullPointerException if either the ID or the user is null
+     */
+    AsynchronousWebRequest<T> getRequest(String requestType, String id, NiFiUser user);
+
+    /**
+     * Removes the request with the given ID
+     *
+     * @param requestType the type of the request being removed
+     * @param id the ID of the request
+     * @param user the user who is retrieving the request
+     * @return the request with the given ID
+     *
+     * @throws ResourceNotFoundException if no request can be found with the given ID
+     * @throws IllegalArgumentException if the user given is not the user that submitted the request
+     * @throws IllegalStateException if the request with the given ID is not yet complete
+     * @throws NullPointerException if either the ID or the user is null
+     */
+    AsynchronousWebRequest<T> removeRequest(String requestType, String id, NiFiUser user);
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
new file mode 100644
index 0000000..8ba9a58
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.web.api.concurrent;
+
+import java.util.Date;
+import java.util.Objects;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+
+public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest<T> {
+    private final String id;
+    private final String processGroupId;
+    private final NiFiUser user;
+
+    private volatile boolean complete = false;
+    private volatile Date lastUpdated = new Date();
+    private volatile String failureReason;
+    private volatile T results;
+
+    public StandardAsynchronousWebRequest(final String requestId, final String processGroupId, final NiFiUser user) {
+        this.id = requestId;
+        this.processGroupId = processGroupId;
+        this.user = user;
+    }
+
+    public String getRequestId() {
+        return id;
+    }
+
+    @Override
+    public boolean isComplete() {
+        return complete;
+    }
+
+    @Override
+    public String getProcessGroupId() {
+        return processGroupId;
+    }
+
+    @Override
+    public void markComplete(final T results) {
+        this.complete = true;
+        this.results = results;
+        this.lastUpdated = new Date();
+    }
+
+    @Override
+    public Date getLastUpdated() {
+        return lastUpdated;
+    }
+
+    @Override
+    public void setLastUpdated(final Date date) {
+        this.lastUpdated = lastUpdated;
+    }
+
+    @Override
+    public NiFiUser getUser() {
+        return user;
+    }
+
+    @Override
+    public void setFailureReason(final String explanation) {
+        this.failureReason = Objects.requireNonNull(explanation);
+        this.complete = true;
+        this.results = null;
+    }
+
+    @Override
+    public String getFailureReason() {
+        return failureReason;
+    }
+
+    @Override
+    public T getResults() {
+        return results;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 58abdea..489e590 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,6 +16,33 @@
  */
 package org.apache.nifi.web.api.dto;
 
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.WebApplicationException;
+
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -112,6 +139,15 @@ import org.apache.nifi.provenance.lineage.LineageEdge;
 import org.apache.nifi.provenance.lineage.LineageNode;
 import org.apache.nifi.provenance.lineage.ProvenanceEventLineageNode;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedLabel;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedPort;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteGroupPort;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteProcessGroup;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateStep;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -160,42 +196,19 @@ import org.apache.nifi.web.api.entity.AllowableValueEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
 import org.apache.nifi.web.api.entity.ConnectionStatusSnapshotEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity;
 import org.apache.nifi.web.api.entity.PortStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusSnapshotEntity;
+import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.TenantEntity;
 import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
-import javax.ws.rs.WebApplicationException;
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
 public final class DtoFactory {
 
     @SuppressWarnings("rawtypes")
@@ -616,6 +629,7 @@ public final class DtoFactory {
         dto.setzIndex(connection.getZIndex());
         dto.setSource(createConnectableDto(connection.getSource()));
         dto.setDestination(createConnectableDto(connection.getDestination()));
+        dto.setVersionedComponentId(connection.getVersionedComponentId().orElse(null));
 
         dto.setBackPressureObjectThreshold(connection.getFlowFileQueue().getBackPressureObjectThreshold());
         dto.setBackPressureDataSizeThreshold(connection.getFlowFileQueue().getBackPressureDataSizeThreshold());
@@ -667,6 +681,7 @@ public final class DtoFactory {
         dto.setId(connectable.getIdentifier());
         dto.setName(isAuthorized ? connectable.getName() : connectable.getIdentifier());
         dto.setType(connectable.getConnectableType().name());
+        dto.setVersionedComponentId(connectable.getVersionedComponentId().orElse(null));
 
         if (connectable instanceof RemoteGroupPort) {
             final RemoteGroupPort remoteGroupPort = (RemoteGroupPort) connectable;
@@ -708,6 +723,7 @@ public final class DtoFactory {
         dto.setWidth(label.getSize().getWidth());
         dto.setLabel(label.getValue());
         dto.setParentGroupId(label.getProcessGroup().getIdentifier());
+        dto.setVersionedComponentId(label.getVersionedComponentId().orElse(null));
 
         return dto;
     }
@@ -824,6 +840,7 @@ public final class DtoFactory {
         dto.setId(funnel.getIdentifier());
         dto.setPosition(createPositionDto(funnel.getPosition()));
         dto.setParentGroupId(funnel.getProcessGroup().getIdentifier());
+        dto.setVersionedComponentId(funnel.getVersionedComponentId().orElse(null));
 
         return dto;
     }
@@ -1228,6 +1245,7 @@ public final class DtoFactory {
         dto.setParentGroupId(port.getProcessGroup().getIdentifier());
         dto.setState(port.getScheduledState().toString());
         dto.setType(port.getConnectableType().name());
+        dto.setVersionedComponentId(port.getVersionedComponentId().orElse(null));
 
         // if this port is on the root group, determine if its actually connected to another nifi
         if (port instanceof RootGroupPort) {
@@ -1354,6 +1372,7 @@ public final class DtoFactory {
         dto.setDeprecated(controllerServiceNode.isDeprecated());
         dto.setExtensionMissing(controllerServiceNode.isExtensionMissing());
         dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+        dto.setVersionedComponentId(controllerServiceNode.getVersionedComponentId().orElse(null));
 
         // sort a copy of the properties
         final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() {
@@ -1511,6 +1530,7 @@ public final class DtoFactory {
         dto.setConcurrentlySchedulableTaskCount(port.getMaxConcurrentTasks());
         dto.setUseCompression(port.isUseCompression());
         dto.setExists(port.getTargetExists());
+        dto.setVersionedComponentId(port.getVersionedComponentId().orElse(null));
 
         final BatchSettingsDTO batchDTO = new BatchSettingsDTO();
         batchDTO.setCount(port.getBatchCount());
@@ -1619,6 +1639,7 @@ public final class DtoFactory {
         dto.setInactiveRemoteInputPortCount(inactiveRemoteInputPortCount);
         dto.setActiveRemoteOutputPortCount(activeRemoteOutputPortCount);
         dto.setInactiveRemoteOutputPortCount(inactiveRemoteOutputPortCount);
+        dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null));
 
         final ProcessGroupCounts counts = group.getCounts();
         if (counts != null) {
@@ -1679,6 +1700,7 @@ public final class DtoFactory {
         dto.setId(componentAuthorizable.getIdentifier());
         dto.setParentGroupId(componentAuthorizable.getProcessGroupIdentifier());
         dto.setName(authorizable.getResource().getName());
+
         return dto;
     }
 
@@ -1738,6 +1760,81 @@ public final class DtoFactory {
         return dto;
     }
 
+    public AffectedComponentEntity createAffectedComponentEntity(final ProcessorEntity processorEntity) {
+        if (processorEntity == null) {
+            return null;
+        }
+
+        final AffectedComponentEntity component = new AffectedComponentEntity();
+        component.setBulletins(processorEntity.getBulletins());
+        component.setId(processorEntity.getId());
+        component.setPermissions(processorEntity.getPermissions());
+        component.setPosition(processorEntity.getPosition());
+        component.setRevision(processorEntity.getRevision());
+        component.setUri(processorEntity.getUri());
+
+        final ProcessorDTO processorDto = processorEntity.getComponent();
+        final AffectedComponentDTO componentDto = new AffectedComponentDTO();
+        componentDto.setId(processorDto.getId());
+        componentDto.setName(processorDto.getName());
+        componentDto.setProcessGroupId(processorDto.getParentGroupId());
+        componentDto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        componentDto.setState(processorDto.getState());
+        componentDto.setValidationErrors(processorDto.getValidationErrors());
+        component.setComponent(componentDto);
+
+        return component;
+    }
+
+    public AffectedComponentEntity createAffectedComponentEntity(final ControllerServiceEntity serviceEntity) {
+        if (serviceEntity == null) {
+            return null;
+        }
+
+        final AffectedComponentEntity component = new AffectedComponentEntity();
+        component.setBulletins(serviceEntity.getBulletins());
+        component.setId(serviceEntity.getId());
+        component.setPermissions(serviceEntity.getPermissions());
+        component.setPosition(serviceEntity.getPosition());
+        component.setRevision(serviceEntity.getRevision());
+        component.setUri(serviceEntity.getUri());
+
+        final ControllerServiceDTO serviceDto = serviceEntity.getComponent();
+        final AffectedComponentDTO componentDto = new AffectedComponentDTO();
+        componentDto.setId(serviceDto.getId());
+        componentDto.setName(serviceDto.getName());
+        componentDto.setProcessGroupId(serviceDto.getParentGroupId());
+        componentDto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        componentDto.setState(serviceDto.getState());
+        componentDto.setValidationErrors(serviceDto.getValidationErrors());
+        component.setComponent(componentDto);
+
+        return component;
+    }
+
+    public AffectedComponentEntity createAffectedComponentEntity(final RemoteProcessGroupPortDTO remotePortDto, final String referenceType, final RemoteProcessGroupEntity rpgEntity) {
+        if (remotePortDto == null) {
+            return null;
+        }
+
+        final AffectedComponentEntity component = new AffectedComponentEntity();
+        component.setId(remotePortDto.getId());
+        component.setPermissions(rpgEntity.getPermissions());
+        component.setRevision(rpgEntity.getRevision());
+        component.setUri(rpgEntity.getUri());
+
+        final AffectedComponentDTO componentDto = new AffectedComponentDTO();
+        componentDto.setId(remotePortDto.getId());
+        componentDto.setName(remotePortDto.getName());
+        componentDto.setProcessGroupId(remotePortDto.getGroupId());
+        componentDto.setReferenceType(referenceType);
+        componentDto.setState(remotePortDto.isTransmitting() ? "Running" : "Stopped");
+        component.setComponent(componentDto);
+
+        return component;
+    }
+
+
     public AffectedComponentDTO createAffectedComponentDto(final ConfiguredComponent component) {
         final AffectedComponentDTO dto = new AffectedComponentDTO();
         dto.setId(component.getIdentifier());
@@ -2047,6 +2144,8 @@ public final class DtoFactory {
         dto.setPosition(createPositionDto(group.getPosition()));
         dto.setComments(group.getComments());
         dto.setName(group.getName());
+        dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null));
+        dto.setVersionControlInformation(createVersionControlInformationDto(group.getVersionControlInformation()));
 
         final Map<String, String> variables = group.getVariableRegistry().getVariableMap().entrySet().stream()
             .collect(Collectors.toMap(entry -> entry.getKey().getName(), entry -> entry.getValue()));
@@ -2070,6 +2169,68 @@ public final class DtoFactory {
         return dto;
     }
 
+    public VersionControlInformationDTO createVersionControlInformationDto(final VersionControlInformation versionControlInfo) {
+        if (versionControlInfo == null) {
+            return null;
+        }
+
+        final VersionControlInformationDTO dto = new VersionControlInformationDTO();
+        dto.setRegistryId(versionControlInfo.getRegistryIdentifier());
+        dto.setBucketId(versionControlInfo.getBucketIdentifier());
+        dto.setFlowId(versionControlInfo.getFlowIdentifier());
+        dto.setVersion(versionControlInfo.getVersion());
+        dto.setCurrent(versionControlInfo.getCurrent().orElse(null));
+        dto.setModified(versionControlInfo.getModified().orElse(null));
+        return dto;
+    }
+
+    public Map<String, String> createVersionControlComponentMappingDto(final InstantiatedVersionedProcessGroup group) {
+        final Map<String, String> mapping = new HashMap<>();
+
+        mapping.put(group.getInstanceId(), group.getIdentifier());
+        group.getProcessors().stream()
+            .map(proc -> (InstantiatedVersionedProcessor) proc)
+            .forEach(proc -> mapping.put(proc.getInstanceId(), proc.getIdentifier()));
+        group.getInputPorts().stream()
+            .map(port -> (InstantiatedVersionedPort) port)
+            .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+        group.getOutputPorts().stream()
+            .map(port -> (InstantiatedVersionedPort) port)
+            .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+        group.getControllerServices().stream()
+            .map(service -> (InstantiatedVersionedControllerService) service)
+            .forEach(service -> mapping.put(service.getInstanceId(), service.getIdentifier()));
+        group.getLabels().stream()
+            .map(label -> (InstantiatedVersionedLabel) label)
+            .forEach(label -> mapping.put(label.getInstanceId(), label.getIdentifier()));
+        group.getConnections().stream()
+            .map(conn -> (InstantiatedVersionedConnection) conn)
+            .forEach(conn -> mapping.put(conn.getInstanceId(), conn.getIdentifier()));
+        group.getRemoteProcessGroups().stream()
+            .map(rpg -> (InstantiatedVersionedRemoteProcessGroup) rpg)
+            .forEach(rpg -> {
+                mapping.put(rpg.getInstanceId(), rpg.getIdentifier());
+
+                rpg.getInputPorts().stream()
+                    .map(port -> (InstantiatedVersionedRemoteGroupPort) port)
+                    .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+
+                rpg.getOutputPorts().stream()
+                    .map(port -> (InstantiatedVersionedRemoteGroupPort) port)
+                    .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+            });
+
+        group.getProcessGroups().stream()
+            .map(child -> (InstantiatedVersionedProcessGroup) child)
+            .forEach(child -> {
+                final Map<String, String> childMapping = createVersionControlComponentMappingDto(child);
+                mapping.putAll(childMapping);
+            });
+
+        return mapping;
+    }
+
+
     /**
      * Creates a ProcessGroupContentDTO from the specified ProcessGroup.
      *
@@ -2418,6 +2579,7 @@ public final class DtoFactory {
         dto.setDeprecated(node.isDeprecated());
         dto.setExtensionMissing(node.isExtensionMissing());
         dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+        dto.setVersionedComponentId(node.getVersionedComponentId().orElse(null));
 
         dto.setType(node.getCanonicalClassName());
         dto.setBundle(createBundleDto(bundleCoordinate));
@@ -2989,6 +3151,7 @@ public final class DtoFactory {
         copy.setPosition(original.getPosition());
         copy.setWidth(original.getWidth());
         copy.setHeight(original.getHeight());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
 
         return copy;
     }
@@ -3012,6 +3175,7 @@ public final class DtoFactory {
         copy.setMultipleVersionsAvailable(original.getMultipleVersionsAvailable());
         copy.setPersistsState(original.getPersistsState());
         copy.setValidationErrors(copy(original.getValidationErrors()));
+        copy.setVersionedComponentId(original.getVersionedComponentId());
         return copy;
     }
 
@@ -3020,6 +3184,7 @@ public final class DtoFactory {
         copy.setId(original.getId());
         copy.setParentGroupId(original.getParentGroupId());
         copy.setPosition(original.getPosition());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
 
         return copy;
     }
@@ -3088,6 +3253,7 @@ public final class DtoFactory {
         copy.setExtensionMissing(original.getExtensionMissing());
         copy.setMultipleVersionsAvailable(original.getMultipleVersionsAvailable());
         copy.setValidationErrors(copy(original.getValidationErrors()));
+        copy.setVersionedComponentId(original.getVersionedComponentId());
 
         return copy;
     }
@@ -3132,6 +3298,7 @@ public final class DtoFactory {
         copy.setzIndex(original.getzIndex());
         copy.setLabelIndex(original.getLabelIndex());
         copy.setBends(copy(original.getBends()));
+        copy.setVersionedComponentId(original.getVersionedComponentId());
 
         return copy;
     }
@@ -3164,6 +3331,7 @@ public final class DtoFactory {
         copy.setUserAccessControl(copy(original.getUserAccessControl()));
         copy.setGroupAccessControl(copy(original.getGroupAccessControl()));
         copy.setValidationErrors(copy(original.getValidationErrors()));
+        copy.setVersionedComponentId(original.getVersionedComponentId());
         return copy;
     }
 
@@ -3180,6 +3348,8 @@ public final class DtoFactory {
         copy.setConcurrentlySchedulableTaskCount(original.getConcurrentlySchedulableTaskCount());
         copy.setUseCompression(original.getUseCompression());
         copy.setExists(original.getExists());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+
         final BatchSettingsDTO batchOrg = original.getBatchSettings();
         if (batchOrg != null) {
             final BatchSettingsDTO batchCopy = new BatchSettingsDTO();
@@ -3199,8 +3369,10 @@ public final class DtoFactory {
         copy.setInputPortCount(original.getInputPortCount());
         copy.setInvalidCount(original.getInvalidCount());
         copy.setName(original.getName());
+        copy.setVersionControlInformation(copy(original.getVersionControlInformation()));
         copy.setOutputPortCount(original.getOutputPortCount());
         copy.setParentGroupId(original.getParentGroupId());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
 
         copy.setRunningCount(original.getRunningCount());
         copy.setStoppedCount(original.getStoppedCount());
@@ -3215,6 +3387,21 @@ public final class DtoFactory {
         return copy;
     }
 
+    public VersionControlInformationDTO copy(final VersionControlInformationDTO original) {
+        if (original == null) {
+            return null;
+        }
+
+        final VersionControlInformationDTO copy = new VersionControlInformationDTO();
+        copy.setRegistryId(original.getRegistryId());
+        copy.setBucketId(original.getBucketId());
+        copy.setFlowId(original.getFlowId());
+        copy.setVersion(original.getVersion());
+        copy.setCurrent(original.getCurrent());
+        copy.setModified(original.getModified());
+        return copy;
+    }
+
     public RemoteProcessGroupDTO copy(final RemoteProcessGroupDTO original) {
         final RemoteProcessGroupContentsDTO originalContents = original.getContents();
         final RemoteProcessGroupContentsDTO copyContents = new RemoteProcessGroupContentsDTO();
@@ -3256,6 +3443,7 @@ public final class DtoFactory {
         copy.setProxyUser(original.getProxyUser());
         copy.setProxyPassword(original.getProxyPassword());
         copy.setLocalNetworkInterface(original.getLocalNetworkInterface());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
 
         copy.setContents(copyContents);
 
@@ -3268,6 +3456,7 @@ public final class DtoFactory {
         connectable.setId(port.getId());
         connectable.setName(port.getName());
         connectable.setType(type.name());
+        connectable.setVersionedComponentId(port.getVersionedComponentId());
         return connectable;
     }
 
@@ -3277,6 +3466,7 @@ public final class DtoFactory {
         connectable.setId(processor.getId());
         connectable.setName(processor.getName());
         connectable.setType(ConnectableType.PROCESSOR.name());
+        connectable.setVersionedComponentId(processor.getVersionedComponentId());
         return connectable;
     }
 
@@ -3285,6 +3475,7 @@ public final class DtoFactory {
         connectable.setGroupId(funnel.getParentGroupId());
         connectable.setId(funnel.getId());
         connectable.setType(ConnectableType.FUNNEL.name());
+        connectable.setVersionedComponentId(funnel.getVersionedComponentId());
         return connectable;
     }
 
@@ -3294,6 +3485,7 @@ public final class DtoFactory {
         connectable.setId(remoteGroupPort.getId());
         connectable.setName(remoteGroupPort.getName());
         connectable.setType(type.name());
+        connectable.setVersionedComponentId(connectable.getVersionedComponentId());
         return connectable;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index 16781c6..dd8d67f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -67,6 +67,7 @@ import org.apache.nifi.web.api.entity.TenantEntity;
 import org.apache.nifi.web.api.entity.UserEntity;
 import org.apache.nifi.web.api.entity.UserGroupEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryEntity;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 
 import java.util.Date;
 import java.util.List;
@@ -537,4 +538,11 @@ public final class EntityFactory {
         }
         return entity;
     }
+
+    public VersionControlInformationEntity createVersionControlInformationEntity(final VersionControlInformationDTO dto, final RevisionDTO processGroupRevision) {
+        final VersionControlInformationEntity entity = new VersionControlInformationEntity();
+        entity.setVersionControlInformation(dto);
+        entity.setProcessGroupRevision(processGroupRevision);
+        return entity;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
index a4e8000..615f00b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
@@ -1599,6 +1599,7 @@ public class ControllerFacade implements Authorizable {
         final List<String> matches = new ArrayList<>();
 
         addIfAppropriate(searchStr, port.getIdentifier(), "Id", matches);
+        addIfAppropriate(searchStr, port.getVersionedComponentId().orElse(null), "Version Control ID", matches);
         addIfAppropriate(searchStr, port.getName(), "Name", matches);
         addIfAppropriate(searchStr, port.getComments(), "Comments", matches);
 
@@ -1649,6 +1650,7 @@ public class ControllerFacade implements Authorizable {
         final Processor processor = procNode.getProcessor();
 
         addIfAppropriate(searchStr, procNode.getIdentifier(), "Id", matches);
+        addIfAppropriate(searchStr, procNode.getVersionedComponentId().orElse(null), "Version Control ID", matches);
         addIfAppropriate(searchStr, procNode.getName(), "Name", matches);
         addIfAppropriate(searchStr, procNode.getComments(), "Comments", matches);
 
@@ -1753,6 +1755,7 @@ public class ControllerFacade implements Authorizable {
         }
 
         addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches);
+        addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches);
         addIfAppropriate(searchStr, group.getName(), "Name", matches);
         addIfAppropriate(searchStr, group.getComments(), "Comments", matches);
 
@@ -1783,6 +1786,7 @@ public class ControllerFacade implements Authorizable {
 
         // search id and name
         addIfAppropriate(searchStr, connection.getIdentifier(), "Id", matches);
+        addIfAppropriate(searchStr, connection.getVersionedComponentId().orElse(null), "Version Control ID", matches);
         addIfAppropriate(searchStr, connection.getName(), "Name", matches);
 
         // search relationships
@@ -1864,6 +1868,7 @@ public class ControllerFacade implements Authorizable {
     private ComponentSearchResultDTO search(final String searchStr, final RemoteProcessGroup group) {
         final List<String> matches = new ArrayList<>();
         addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches);
+        addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches);
         addIfAppropriate(searchStr, group.getName(), "Name", matches);
         addIfAppropriate(searchStr, group.getComments(), "Comments", matches);
         addIfAppropriate(searchStr, group.getTargetUris(), "URLs", matches);
@@ -1889,6 +1894,7 @@ public class ControllerFacade implements Authorizable {
     private ComponentSearchResultDTO search(final String searchStr, final Funnel funnel) {
         final List<String> matches = new ArrayList<>();
         addIfAppropriate(searchStr, funnel.getIdentifier(), "Id", matches);
+        addIfAppropriate(searchStr, funnel.getVersionedComponentId().orElse(null), "Version Control ID", matches);
 
         if (matches.isEmpty()) {
             return null;

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index d7ca806..5f4dba5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -16,14 +16,17 @@
  */
 package org.apache.nifi.web.dao;
 
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Future;
 
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 
 public interface ProcessGroupDAO {
 
@@ -104,6 +107,27 @@ public interface ProcessGroupDAO {
     ProcessGroup updateProcessGroup(ProcessGroupDTO processGroup);
 
     /**
+     * Updates the process group so that it matches the proposed flow
+     *
+     * @param groupId the ID of the process group
+     * @param proposedSnapshot Flow the new version of the flow
+     * @param versionControlInformation the new Version Control Information
+     * @param the seed value to use for generating ID's for new components
+     * @return the process group
+     */
+    ProcessGroup updateProcessGroupFlow(String groupId, VersionedFlowSnapshot proposedSnapshot, VersionControlInformationDTO versionControlInformation, String componentIdSeed,
+        boolean verifyNotModified);
+
+    /**
+     * Applies the given Version Control Information to the Process Group
+     *
+     * @param versionControlInformation the Version Control Information to apply
+     * @param versionedComponentMapping a mapping of Component ID to Versioned Component ID
+     * @return the Process Group
+     */
+    ProcessGroup updateVersionControlInformation(VersionControlInformationDTO versionControlInformation, Map<String, String> versionedComponentMapping);
+
+    /**
      * Updates the specified variable registry
      *
      * @param variableRegistry the Variable Registry

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index ec584de..6fa316d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -16,15 +16,14 @@
  */
 package org.apache.nifi.web.dao.impl;
 
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 
 import org.apache.nifi.connectable.Connectable;
-import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.connectable.Position;
 import org.apache.nifi.controller.FlowController;
@@ -33,9 +32,14 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.StandardVersionControlInformation;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.dao.ProcessGroupDAO;
 
@@ -90,24 +94,30 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     public void verifyScheduleComponents(final String groupId, final ScheduledState state,final Set<String> componentIds) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
 
-        final Set<Connectable> connectables = new HashSet<>(componentIds.size());
         for (final String componentId : componentIds) {
             final Connectable connectable = group.findLocalConnectable(componentId);
             if (connectable == null) {
-                throw new ResourceNotFoundException("Unable to find component with id " + componentId);
-            }
+                final RemoteGroupPort remotePort = group.findRemoteGroupPort(componentId);
+                if (remotePort == null) {
+                    throw new ResourceNotFoundException("Unable to find component with id " + componentId);
+                }
 
-            connectables.add(connectable);
-        }
+                if (ScheduledState.RUNNING.equals(state)) {
+                    remotePort.verifyCanStart();
+                } else {
+                    remotePort.verifyCanStop();
+                }
 
-        // verify as appropriate
-        connectables.forEach(connectable -> {
+                continue;
+            }
+
+            // verify as appropriate
             if (ScheduledState.RUNNING.equals(state)) {
                 group.verifyCanStart(connectable);
             } else {
                 group.verifyCanStop(connectable);
             }
-        });
+        }
     }
 
     @Override
@@ -134,22 +144,46 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         for (final String componentId : componentIds) {
             final Connectable connectable = group.findLocalConnectable(componentId);
             if (ScheduledState.RUNNING.equals(state)) {
-                if (ConnectableType.PROCESSOR.equals(connectable.getConnectableType())) {
-                    final CompletableFuture<?> processorFuture = connectable.getProcessGroup().startProcessor((ProcessorNode) connectable, true);
-                    future = CompletableFuture.allOf(future, processorFuture);
-                } else if (ConnectableType.INPUT_PORT.equals(connectable.getConnectableType())) {
-                    connectable.getProcessGroup().startInputPort((Port) connectable);
-                } else if (ConnectableType.OUTPUT_PORT.equals(connectable.getConnectableType())) {
-                    connectable.getProcessGroup().startOutputPort((Port) connectable);
+                switch (connectable.getConnectableType()) {
+                    case PROCESSOR:
+                        final CompletableFuture<?> processorFuture = connectable.getProcessGroup().startProcessor((ProcessorNode) connectable, true);
+                        future = CompletableFuture.allOf(future, processorFuture);
+                        break;
+                    case INPUT_PORT:
+                        connectable.getProcessGroup().startInputPort((Port) connectable);
+                        break;
+                    case OUTPUT_PORT:
+                        connectable.getProcessGroup().startOutputPort((Port) connectable);
+                        break;
+                    case REMOTE_INPUT_PORT:
+                        final RemoteGroupPort remoteInputPort = group.findRemoteGroupPort(componentId);
+                        remoteInputPort.getRemoteProcessGroup().startTransmitting(remoteInputPort);
+                        break;
+                    case REMOTE_OUTPUT_PORT:
+                        final RemoteGroupPort remoteOutputPort = group.findRemoteGroupPort(componentId);
+                        remoteOutputPort.getRemoteProcessGroup().startTransmitting(remoteOutputPort);
+                        break;
                 }
             } else {
-                if (ConnectableType.PROCESSOR.equals(connectable.getConnectableType())) {
-                    final CompletableFuture<?> processorFuture = connectable.getProcessGroup().stopProcessor((ProcessorNode) connectable);
-                    future = CompletableFuture.allOf(future, processorFuture);
-                } else if (ConnectableType.INPUT_PORT.equals(connectable.getConnectableType())) {
-                    connectable.getProcessGroup().stopInputPort((Port) connectable);
-                } else if (ConnectableType.OUTPUT_PORT.equals(connectable.getConnectableType())) {
-                    connectable.getProcessGroup().stopOutputPort((Port) connectable);
+                switch (connectable.getConnectableType()) {
+                    case PROCESSOR:
+                        final CompletableFuture<?> processorFuture = connectable.getProcessGroup().stopProcessor((ProcessorNode) connectable);
+                        future = CompletableFuture.allOf(future, processorFuture);
+                        break;
+                    case INPUT_PORT:
+                        connectable.getProcessGroup().stopInputPort((Port) connectable);
+                        break;
+                    case OUTPUT_PORT:
+                        connectable.getProcessGroup().stopOutputPort((Port) connectable);
+                        break;
+                    case REMOTE_INPUT_PORT:
+                        final RemoteGroupPort remoteInputPort = group.findRemoteGroupPort(componentId);
+                        remoteInputPort.getRemoteProcessGroup().stopTransmitting(remoteInputPort);
+                        break;
+                    case REMOTE_OUTPUT_PORT:
+                        final RemoteGroupPort remoteOutputPort = group.findRemoteGroupPort(componentId);
+                        remoteOutputPort.getRemoteProcessGroup().stopTransmitting(remoteOutputPort);
+                        break;
                 }
             }
         }
@@ -197,6 +231,41 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
+    public ProcessGroup updateVersionControlInformation(final VersionControlInformationDTO versionControlInformation, final Map<String, String> versionedComponentMapping) {
+        final String groupId = versionControlInformation.getGroupId();
+        final ProcessGroup group = locateProcessGroup(flowController, groupId);
+
+        final String registryId = versionControlInformation.getRegistryId();
+        final String bucketId = versionControlInformation.getBucketId();
+        final String flowId = versionControlInformation.getFlowId();
+        final int version = versionControlInformation.getVersion();
+
+        final VersionControlInformation vci = new StandardVersionControlInformation(registryId, bucketId, flowId, version, null, false, true);
+        group.setVersionControlInformation(vci, versionedComponentMapping);
+
+        return group;
+    }
+
+    @Override
+    public ProcessGroup updateProcessGroupFlow(final String groupId, final VersionedFlowSnapshot proposedSnapshot, final VersionControlInformationDTO versionControlInformation,
+        final String componentIdSeed, final boolean verifyNotModified) {
+        final ProcessGroup group = locateProcessGroup(flowController, groupId);
+        group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified);
+
+        final StandardVersionControlInformation svci = new StandardVersionControlInformation(
+            versionControlInformation.getRegistryId(),
+            versionControlInformation.getBucketId(),
+            versionControlInformation.getFlowId(),
+            versionControlInformation.getVersion(),
+            proposedSnapshot.getFlowContents(),
+            versionControlInformation.getModified(),
+            versionControlInformation.getCurrent());
+
+        group.setVersionControlInformation(svci, Collections.emptyMap());
+        return group;
+    }
+
+    @Override
     public ProcessGroup updateVariableRegistry(final VariableRegistryDTO variableRegistry) {
         final ProcessGroup group = locateProcessGroup(flowController, variableRegistry.getProcessGroupId());
         if (group == null) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
new file mode 100644
index 0000000..7fdaf56
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
@@ -0,0 +1,71 @@
+/*
+ * 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.web.util;
+
+import java.util.Optional;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.web.NiFiServiceFacade;
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.DtoFactory;
+import org.apache.nifi.web.api.dto.RemoteProcessGroupContentsDTO;
+import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
+import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
+
+public class AffectedComponentUtils {
+
+    public static AffectedComponentEntity updateEntity(final AffectedComponentEntity componentEntity, final NiFiServiceFacade serviceFacade,
+                final DtoFactory dtoFactory, final NiFiUser user) {
+
+        switch (componentEntity.getComponent().getReferenceType()) {
+            case AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR:
+                final ProcessorEntity procEntity = serviceFacade.getProcessor(componentEntity.getId(), user);
+                return dtoFactory.createAffectedComponentEntity(procEntity);
+            case AffectedComponentDTO.COMPONENT_TYPE_REMOTE_INPUT_PORT: {
+                final RemoteProcessGroupEntity remoteGroupEntity = serviceFacade.getRemoteProcessGroup(componentEntity.getComponent().getProcessGroupId(), user);
+                final RemoteProcessGroupContentsDTO remoteGroupContents = remoteGroupEntity.getComponent().getContents();
+                final Optional<RemoteProcessGroupPortDTO> portDtoOption = remoteGroupContents.getInputPorts().stream()
+                    .filter(port -> port.getId().equals(componentEntity.getId()))
+                    .findFirst();
+
+                if (portDtoOption.isPresent()) {
+                    final RemoteProcessGroupPortDTO portDto = portDtoOption.get();
+                    return dtoFactory.createAffectedComponentEntity(portDto, AffectedComponentDTO.COMPONENT_TYPE_REMOTE_INPUT_PORT, remoteGroupEntity);
+                }
+                break;
+            }
+            case AffectedComponentDTO.COMPONENT_TYPE_REMOTE_OUTPUT_PORT: {
+                final RemoteProcessGroupEntity remoteGroupEntity = serviceFacade.getRemoteProcessGroup(componentEntity.getComponent().getProcessGroupId(), user);
+                final RemoteProcessGroupContentsDTO remoteGroupContents = remoteGroupEntity.getComponent().getContents();
+                final Optional<RemoteProcessGroupPortDTO> portDtoOption = remoteGroupContents.getOutputPorts().stream()
+                    .filter(port -> port.getId().equals(componentEntity.getId()))
+                    .findFirst();
+
+                if (portDtoOption.isPresent()) {
+                    final RemoteProcessGroupPortDTO portDto = portDtoOption.get();
+                    return dtoFactory.createAffectedComponentEntity(portDto, AffectedComponentDTO.COMPONENT_TYPE_REMOTE_OUTPUT_PORT, remoteGroupEntity);
+                }
+                break;
+            }
+        }
+
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
new file mode 100644
index 0000000..a6efb71
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
@@ -0,0 +1,59 @@
+/*
+ * 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.web.util;
+
+import java.util.concurrent.TimeUnit;
+
+public class CancellableTimedPause implements Pause {
+    private final long expirationNanoTime;
+    private final long pauseNanos;
+    private volatile boolean cancelled = false;
+
+    public CancellableTimedPause(final long pauseTime, final long expirationTime, final TimeUnit timeUnit) {
+        final long expirationNanos = TimeUnit.NANOSECONDS.convert(expirationTime, timeUnit);
+        expirationNanoTime = System.nanoTime() + expirationNanos;
+        pauseNanos = Math.max(1L, TimeUnit.NANOSECONDS.convert(pauseTime, timeUnit));
+    }
+
+    public void cancel() {
+        cancelled = true;
+    }
+
+    @Override
+    public boolean pause() {
+        if (cancelled) {
+            return false;
+        }
+
+        long sysTime = System.nanoTime();
+        final long maxWaitTime = System.nanoTime() + pauseNanos;
+        while (sysTime < maxWaitTime) {
+            try {
+                TimeUnit.NANOSECONDS.wait(pauseNanos);
+            } catch (final InterruptedException ie) {
+                Thread.currentThread().interrupt();
+                return false;
+            }
+
+            sysTime = System.nanoTime();
+        }
+
+        return sysTime < expirationNanoTime && !cancelled;
+    }
+
+}


[15/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connectable.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connectable.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connectable.java
index 94e0745..a91ce97 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connectable.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connectable.java
@@ -18,6 +18,7 @@ package org.apache.nifi.connectable;
 
 import org.apache.nifi.authorization.resource.ComponentAuthorizable;
 import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.controller.Triggerable;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.processor.ProcessSession;
@@ -32,11 +33,12 @@ import java.util.concurrent.TimeUnit;
 /**
  * Represents a connectable component to which or from which data can flow.
  */
-public interface Connectable extends Triggerable, ComponentAuthorizable, Positionable {
+public interface Connectable extends Triggerable, ComponentAuthorizable, Positionable, VersionedComponent {
 
     /**
      * @return the unique identifier for this <code>Connectable</code>
      */
+    @Override
     String getIdentifier();
 
     /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connection.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connection.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connection.java
index acdcec6..423f52d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connection.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/connectable/Connection.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.connectable;
 
 import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.controller.queue.FlowFileQueue;
 import org.apache.nifi.controller.repository.FlowFileRecord;
 import org.apache.nifi.groups.ProcessGroup;
@@ -27,7 +28,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
-public interface Connection extends Authorizable {
+public interface Connection extends Authorizable, VersionedComponent {
 
     void enqueue(FlowFileRecord flowFile);
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractPort.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractPort.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractPort.java
index 7190fd4..0240648 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractPort.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractPort.java
@@ -42,6 +42,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -81,6 +82,7 @@ public abstract class AbstractPort implements Port {
     private final AtomicReference<String> penalizationPeriod;
     private final AtomicReference<String> yieldPeriod;
     private final AtomicReference<String> schedulingPeriod;
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
     private final AtomicLong schedulingNanos;
     private final AtomicLong yieldExpiration;
     private final ProcessScheduler processScheduler;
@@ -635,4 +637,27 @@ public abstract class AbstractPort implements Port {
     @Override
     public void verifyCanClearState() {
     }
+
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/StandardFunnel.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/StandardFunnel.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/StandardFunnel.java
index 34ffbac..4b3507c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/StandardFunnel.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/StandardFunnel.java
@@ -43,6 +43,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -77,6 +78,7 @@ public class StandardFunnel implements Funnel {
     private final AtomicBoolean lossTolerant;
     private final AtomicReference<ScheduledState> scheduledState;
     private final AtomicLong yieldExpiration;
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
 
     private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
     private final Lock readLock = rwLock.readLock();
@@ -557,4 +559,27 @@ public class StandardFunnel implements Funnel {
     public String getComponentType() {
         return "Funnel";
     }
+
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/label/Label.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/label/Label.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/label/Label.java
index bc1be00..d463725 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/label/Label.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/label/Label.java
@@ -17,14 +17,16 @@
 package org.apache.nifi.controller.label;
 
 import org.apache.nifi.authorization.resource.ComponentAuthorizable;
+import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.connectable.Positionable;
 import org.apache.nifi.connectable.Size;
 import org.apache.nifi.groups.ProcessGroup;
 
 import java.util.Map;
 
-public interface Label extends ComponentAuthorizable, Positionable {
+public interface Label extends ComponentAuthorizable, Positionable, VersionedComponent {
 
+    @Override
     String getIdentifier();
 
     Map<String, String> getStyle();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
index 3dd1076..2f28963 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.controller.service;
 
+import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.controller.ConfiguredComponent;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.controller.LoggableComponent;
@@ -26,7 +27,7 @@ import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
 
-public interface ControllerServiceNode extends ConfiguredComponent {
+public interface ControllerServiceNode extends ConfiguredComponent, VersionedComponent {
 
     /**
      * @return the Process Group that this Controller Service belongs to, or <code>null</code> if the Controller Service

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index 0baba23..8934788 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.groups;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -24,6 +25,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.function.Predicate;
 
 import org.apache.nifi.authorization.resource.ComponentAuthorizable;
+import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
@@ -39,6 +41,10 @@ import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.UnknownResourceException;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.remote.RemoteGroupPort;
 
 /**
@@ -50,7 +56,7 @@ import org.apache.nifi.remote.RemoteGroupPort;
  * <p>
  * MUST BE THREAD-SAFE</p>
  */
-public interface ProcessGroup extends ComponentAuthorizable, Positionable {
+public interface ProcessGroup extends ComponentAuthorizable, Positionable, VersionedComponent {
 
     /**
      * Predicate for filtering schedulable Processors.
@@ -772,6 +778,17 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable {
     void move(final Snippet snippet, final ProcessGroup destination);
 
     /**
+     * Updates the Process Group to match the proposed flow
+     *
+     * @param proposedSnapshot the proposed flow
+     * @param componentIdSeed a seed value to use when generating ID's for new components
+     * @param verifyNotDirty whether or not to verify that the Process Group is not 'dirty'. If this value is <code>true</code>,
+     *            and the Process Group has been modified since it was last synchronized with the Flow Registry, then this method will
+     *            throw an IllegalStateException
+     */
+    void updateFlow(VersionedFlowSnapshot proposedSnapshot, String componentIdSeed, boolean verifyNotDirty);
+
+    /**
      * Verifies a template with the specified name can be created.
      *
      * @param name name of the template
@@ -832,6 +849,18 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable {
     void verifyCanUpdateVariables(Map<String, String> updatedVariables);
 
     /**
+     * Ensure that the contents of the Process Group can be update to match the given new flow
+     *
+     * @param updatedFlow the updated version of the flow
+     * @param verifyConnectionRemoval whether or not to verify that connections that are not present in the updated flow can be removed
+     * @param verifyNotDirty whether or not to verify that the Process Group is not 'dirty'. If <code>true</code> and the Process Group has been changed since
+     *            it was last synchronized with the FlowRegistry, then this method will throw an IllegalStateException
+     *
+     * @throws IllegalStateException if the Process Group is not in a state that will allow the update
+     */
+    void verifyCanUpdate(VersionedFlowSnapshot updatedFlow, boolean verifyConnectionRemoval, boolean verifyNotDirty);
+
+    /**
      * Adds the given template to this Process Group
      *
      * @param template the template to add
@@ -894,4 +923,30 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable {
      * @return a set of all components that are affected by the variable with the given name
      */
     Set<ConfiguredComponent> getComponentsAffectedByVariable(String variableName);
+
+    /**
+     * @return the version control information that indicates where this flow is stored in a Flow Registry,
+     *         or <code>null</code> if this Process Group is not under version control.
+     */
+    VersionControlInformation getVersionControlInformation();
+
+    /**
+     * Updates the Version Control Information for this Process Group
+     *
+     * @param versionControlInformation specification of where the flow is tracked in Version Control
+     * @param versionedComponentIds a mapping of component ID's to Versioned Component ID's. This is used to update the components in the
+     *            Process Group so that the components that exist in the Process Group can be associated with the corresponding components in the
+     *            Version Controlled flow
+     */
+    void setVersionControlInformation(VersionControlInformation versionControlInformation, Map<String, String> versionedComponentIds);
+
+    /**
+     * Synchronizes the Process Group with the given Flow Registry, determining whether or not the local flow
+     * is up to date with the newest version of the flow in the Registry and whether or not the local flow has been
+     * modified since it was last synced with the Flow Registry. If this Process Group is not under Version Control,
+     * this method will have no effect.
+     *
+     * @param flowRegistry the Flow Registry to synchronize with
+     */
+    void synchronizeWithFlowRegistry(FlowRegistryClient flowRegistry);
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
index 79b9509..e4da31b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
@@ -18,6 +18,7 @@ package org.apache.nifi.groups;
 
 import org.apache.nifi.authorization.resource.ComponentAuthorizable;
 import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.VersionedComponent;
 import org.apache.nifi.connectable.Positionable;
 import org.apache.nifi.controller.exception.CommunicationsException;
 import org.apache.nifi.events.EventReporter;
@@ -30,7 +31,7 @@ import java.util.Date;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-public interface RemoteProcessGroup extends ComponentAuthorizable, Positionable {
+public interface RemoteProcessGroup extends ComponentAuthorizable, Positionable, VersionedComponent {
 
     @Override
     String getIdentifier();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupPortDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupPortDescriptor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupPortDescriptor.java
index b797749..2f9a9fd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupPortDescriptor.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupPortDescriptor.java
@@ -40,6 +40,11 @@ public interface RemoteProcessGroupPortDescriptor {
     String getTargetId();
 
     /**
+     * @return the ID corresponding to the component that is under version control
+     */
+    String getVersionedComponentId();
+
+    /**
      * @return id of the remote process group that this port resides in
      */
     String getGroupId();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
new file mode 100644
index 0000000..a5bb738
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -0,0 +1,93 @@
+/*
+ * 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.registry.flow;
+
+import java.io.IOException;
+
+public interface FlowRegistry {
+
+    /**
+     * @return the URL of the Flow Registry
+     */
+    String getURL();
+
+    /**
+     * Registers the given Versioned Flow with the Flow Registry
+     *
+     * @param flow the Versioned Flow to add to the registry
+     * @return the fully populated VersionedFlow
+     *
+     * @throws NullPointerException if the VersionedFlow is null, or if its bucket identifier or name is null
+     * @throws UnknownResourceException if the bucket id does not exist
+     */
+    VersionedFlow registerVersionedFlow(VersionedFlow flow) throws IOException, UnknownResourceException;
+
+    /**
+     * Adds the given snapshot to the Flow Registry for the given flow
+     *
+     * @param flow the Versioned Flow
+     * @param snapshot the snapshot of the flow
+     * @param comments any comments for the snapshot
+     * @return the versioned flow snapshot
+     *
+     * @throws IOException if unable to communicate with the registry
+     * @throws NullPointerException if the VersionedFlow is null, or if its bucket identifier is null, or if the flow to snapshot is null
+     * @throws UnknownResourceException if the flow does not exist
+     */
+    VersionedFlowSnapshot registerVersionedFlowSnapshot(VersionedFlow flow, VersionedProcessGroup snapshot, String comments) throws IOException, UnknownResourceException;
+
+    /**
+     * Returns the latest (most recent) version of the Flow in the Flow Registry for the given bucket and flow
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @return the latest version of the Flow
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws UnknownResourceException if unable to find the bucket with the given ID or the flow with the given ID
+     */
+    int getLatestVersion(String bucketId, String flowId) throws IOException, UnknownResourceException;
+
+    /**
+     * Retrieves the contents of the Flow with the given Bucket ID, Flow ID, and version, from the Flow Registry
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @param version the version to retrieve
+     * @return the contents of the Flow from the Flow Registry
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws UnknownResourceException if unable to find the contents of the flow due to the bucket or flow not existing,
+     *             or the specified version of the flow not existing
+     * @throws NullPointerException if any of the arguments is not specified
+     * @throws IllegalArgumentException if the given version is less than 1
+     */
+    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version) throws IOException, UnknownResourceException;
+
+    /**
+     * Retrieves a VersionedFlow by bucket id and flow id
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @return the VersionedFlow for the given bucket and flow ID's
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws UnknownResourceException if unable to find a flow with the given bucket ID and flow ID
+     */
+    VersionedFlow getVersionedFlow(String bucketId, String flowId) throws IOException, UnknownResourceException;
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
new file mode 100644
index 0000000..83f66dc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
@@ -0,0 +1,37 @@
+/*
+ * 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.registry.flow;
+
+import java.util.Set;
+
+public interface FlowRegistryClient {
+    FlowRegistry getFlowRegistry(String registryId);
+
+    default String getFlowRegistryId(String registryUrl) {
+        for (final String registryClientId : getRegistryIdentifiers()) {
+            final FlowRegistry registry = getFlowRegistry(registryClientId);
+            if (registry.getURL().equals(registryUrl)) {
+                return registryClientId;
+            }
+        }
+
+        return null;
+    }
+
+    Set<String> getRegistryIdentifiers();
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java
new file mode 100644
index 0000000..8c95e67
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/UnknownResourceException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.registry.flow;
+
+public class UnknownResourceException extends Exception {
+
+    public UnknownResourceException(String message) {
+        super(message);
+    }
+
+    public UnknownResourceException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnknownResourceException(Throwable cause) {
+        super(cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
new file mode 100644
index 0000000..ea70b1c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
@@ -0,0 +1,69 @@
+/*
+ * 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.registry.flow;
+
+import java.util.Optional;
+
+/**
+ * <p>
+ * Provides a mechanism for conveying which Flow Registry a flow is stored in, and
+ * where in the Flow Registry the flow is stored.
+ * </p>
+ */
+public interface VersionControlInformation {
+
+    /**
+     * @return the unique identifier of the Flow Registry that this flow is tracking to
+     */
+    String getRegistryIdentifier();
+
+    /**
+     * @return the unique identifier of the bucket that this flow belongs to
+     */
+    String getBucketIdentifier();
+
+    /**
+     * @return the unique identifier of this flow in the Flow Registry
+     */
+    String getFlowIdentifier();
+
+    /**
+     * @return the version of the flow in the Flow Registry that this flow is based on.
+     */
+    int getVersion();
+
+    /**
+     * @return <code>true</code> if the flow has been modified since the last time that it was updated from the Flow Registry or saved
+     *         to the Flow Registry; <code>false</code> if the flow is in sync with the Flow Registry. An empty optional will be returned
+     *         if it is not yet known whether or not the flow has been modified (for example, on startup, when the flow has not yet been
+     *         fetched from the Flow Registry)
+     */
+    Optional<Boolean> getModified();
+
+    /**
+     * @return <code>true</code> if this version of the flow is the most recent version of the flow available in the Flow Registry.
+     *         An empty optional will be returned if it is not yet known whether or not the flow has been modified (for example, on startup,
+     *         when the flow has not yet been fetched from the Flow Registry)
+     */
+    Optional<Boolean> getCurrent();
+
+    /**
+     * @return the snapshot of the flow that was synchronized with the Flow Registry
+     */
+    VersionedProcessGroup getFlowSnapshot();
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
index d53eb49..09d032e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
@@ -152,6 +152,14 @@
             <groupId>org.apache.curator</groupId>
             <artifactId>curator-recipes</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-data-model</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-flow-diff</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.curator</groupId>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
index 728e8cf..7aa3003 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/connectable/StandardConnection.java
@@ -47,6 +47,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -71,6 +72,7 @@ public final class StandardConnection implements Connection {
     private final StandardFlowFileQueue flowFileQueue;
     private final AtomicInteger labelIndex = new AtomicInteger(1);
     private final AtomicLong zIndex = new AtomicLong(0L);
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
     private final ProcessScheduler scheduler;
     private final int hashCode;
 
@@ -519,4 +521,27 @@ public final class StandardConnection implements Connection {
             }
         }
     }
+
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 56b2590..242ef6a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -164,6 +164,7 @@ import org.apache.nifi.provenance.ProvenanceRepository;
 import org.apache.nifi.provenance.StandardProvenanceEventRecord;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.registry.variable.StandardComponentVariableRegistry;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
@@ -329,6 +330,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
     private final List<RemoteGroupPort> startRemoteGroupPortsAfterInitialization;
     private final LeaderElectionManager leaderElectionManager;
     private final ClusterCoordinator clusterCoordinator;
+    private final FlowRegistryClient flowRegistryClient;
 
     /**
      * true if controller is configured to operate in a clustered environment
@@ -395,7 +397,8 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
             final AuditService auditService,
             final StringEncryptor encryptor,
             final BulletinRepository bulletinRepo,
-            final VariableRegistry variableRegistry) {
+            final VariableRegistry variableRegistry,
+            final FlowRegistryClient flowRegistryClient) {
 
         return new FlowController(
                 flowFileEventRepo,
@@ -409,7 +412,8 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 /* cluster coordinator */ null,
                 /* heartbeat monitor */ null,
                 /* leader election manager */ null,
-                /* variable registry */ variableRegistry);
+                /* variable registry */ variableRegistry,
+                flowRegistryClient);
     }
 
     public static FlowController createClusteredInstance(
@@ -423,7 +427,8 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
             final ClusterCoordinator clusterCoordinator,
             final HeartbeatMonitor heartbeatMonitor,
             final LeaderElectionManager leaderElectionManager,
-            final VariableRegistry variableRegistry) {
+            final VariableRegistry variableRegistry,
+            final FlowRegistryClient flowRegistryClient) {
 
         final FlowController flowController = new FlowController(
                 flowFileEventRepo,
@@ -437,7 +442,8 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 clusterCoordinator,
                 heartbeatMonitor,
                 leaderElectionManager,
-                variableRegistry);
+                variableRegistry,
+                flowRegistryClient);
 
         return flowController;
     }
@@ -454,7 +460,8 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
             final ClusterCoordinator clusterCoordinator,
             final HeartbeatMonitor heartbeatMonitor,
             final LeaderElectionManager leaderElectionManager,
-            final VariableRegistry variableRegistry) {
+            final VariableRegistry variableRegistry,
+            final FlowRegistryClient flowRegistryClient) {
 
         maxTimerDrivenThreads = new AtomicInteger(10);
         maxEventDrivenThreads = new AtomicInteger(5);
@@ -516,6 +523,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         startRemoteGroupPortsAfterInitialization = new ArrayList<>();
         this.authorizer = authorizer;
         this.auditService = auditService;
+        this.flowRegistryClient = flowRegistryClient;
 
         final String gracefulShutdownSecondsVal = nifiProperties.getProperty(GRACEFUL_SHUTDOWN_PERIOD);
         long shutdownSecs;
@@ -754,6 +762,23 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 }
             }, 0L, 30L, TimeUnit.SECONDS);
 
+            timerDrivenEngineRef.get().scheduleWithFixedDelay(new Runnable() {
+                @Override
+                public void run() {
+                    final ProcessGroup rootGroup = getRootGroup();
+                    final List<ProcessGroup> allGroups = rootGroup.findAllProcessGroups();
+                    allGroups.add(rootGroup);
+
+                    for (final ProcessGroup group : allGroups) {
+                        try {
+                            group.synchronizeWithFlowRegistry(flowRegistryClient);
+                        } catch (final Exception e) {
+                            LOG.error("Failed to synchronize {} with Flow Registry", group, e);
+                        }
+                    }
+                }
+            }, 5, 60, TimeUnit.SECONDS);
+
             initialized.set(true);
         } finally {
             writeLock.unlock();
@@ -3311,6 +3336,10 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         return new HashSet<>(reportingTasks.values());
     }
 
+    public FlowRegistryClient getFlowRegistryClient() {
+        return flowRegistryClient;
+    }
+
     @Override
     public ControllerServiceNode createControllerService(final String type, final String id, final BundleCoordinate bundleCoordinate, final Set<URL> additionalUrls, final boolean firstTimeAdded) {
         final ControllerServiceNode serviceNode = controllerServiceProvider.createControllerService(type, id, bundleCoordinate, additionalUrls, firstTimeAdded);

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index 3a0b093..e879e38 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -85,6 +85,8 @@ import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.SimpleProcessLogger;
+import org.apache.nifi.registry.flow.StandardVersionControlInformation;
+import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
@@ -113,6 +115,7 @@ import org.apache.nifi.web.api.dto.ProcessorDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
 import org.apache.nifi.web.api.dto.TemplateDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
@@ -1048,6 +1051,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
         final ProcessGroupDTO processGroupDTO = FlowFromDOMFactory.getProcessGroup(parentId, processGroupElement, encryptor, encodingVersion);
         final ProcessGroup processGroup = controller.createProcessGroup(processGroupDTO.getId());
         processGroup.setComments(processGroupDTO.getComments());
+        processGroup.setVersionedComponentId(processGroupDTO.getVersionedComponentId());
         processGroup.setPosition(toPosition(processGroupDTO.getPosition()));
         processGroup.setName(processGroupDTO.getName());
         processGroup.setParent(parentGroup);
@@ -1072,6 +1076,20 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
 
         processGroup.setVariables(variables);
 
+        final VersionControlInformationDTO versionControlInfoDto = processGroupDTO.getVersionControlInformation();
+        if (versionControlInfoDto != null) {
+            final String registryId = versionControlInfoDto.getRegistryId();
+            final String bucketId = versionControlInfoDto.getBucketId();
+            final String flowId = versionControlInfoDto.getFlowId();
+            final int version = versionControlInfoDto.getVersion();
+            final boolean modified = false;
+            final boolean current = true;
+
+            final VersionControlInformation versionControlInformation = new StandardVersionControlInformation(registryId, bucketId, flowId, version, null, modified, current);
+            // pass empty map for the version control mapping because the VersionedComponentId has already been set on the components
+            processGroup.setVersionControlInformation(versionControlInformation, Collections.emptyMap());
+        }
+
         // Add Controller Services
         final List<Element> serviceNodeList = getChildrenByTagName(processGroupElement, "controllerService");
         if (!serviceNodeList.isEmpty()) {
@@ -1097,6 +1115,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
             }
 
             final ProcessorNode procNode = controller.createProcessor(processorDTO.getType(), processorDTO.getId(), coordinate, false);
+            procNode.setVersionedComponentId(processorDTO.getVersionedComponentId());
             processGroup.addProcessor(procNode);
             updateProcessor(procNode, processorDTO, processGroup, controller);
         }
@@ -1113,6 +1132,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
                 port = controller.createLocalInputPort(portDTO.getId(), portDTO.getName());
             }
 
+            port.setVersionedComponentId(portDTO.getVersionedComponentId());
             port.setPosition(toPosition(portDTO.getPosition()));
             port.setComments(portDTO.getComments());
             port.setProcessGroup(processGroup);
@@ -1156,6 +1176,8 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
             } else {
                 port = controller.createLocalOutputPort(portDTO.getId(), portDTO.getName());
             }
+
+            port.setVersionedComponentId(portDTO.getVersionedComponentId());
             port.setPosition(toPosition(portDTO.getPosition()));
             port.setComments(portDTO.getComments());
             port.setProcessGroup(processGroup);
@@ -1193,6 +1215,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
         for (final Element funnelElement : funnelNodeList) {
             final FunnelDTO funnelDTO = FlowFromDOMFactory.getFunnel(funnelElement);
             final Funnel funnel = controller.createFunnel(funnelDTO.getId());
+            funnel.setVersionedComponentId(funnelDTO.getVersionedComponentId());
             funnel.setPosition(toPosition(funnelDTO.getPosition()));
 
             // Since this is called during startup, we want to add the funnel without enabling it
@@ -1207,6 +1230,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
         for (final Element labelElement : labelNodeList) {
             final LabelDTO labelDTO = FlowFromDOMFactory.getLabel(labelElement);
             final Label label = controller.createLabel(labelDTO.getId(), labelDTO.getLabel());
+            label.setVersionedComponentId(labelDTO.getVersionedComponentId());
             label.setStyle(labelDTO.getStyle());
 
             label.setPosition(toPosition(labelDTO.getPosition()));
@@ -1225,6 +1249,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
         for (final Element remoteProcessGroupElement : remoteProcessGroupNodeList) {
             final RemoteProcessGroupDTO remoteGroupDto = FlowFromDOMFactory.getRemoteProcessGroup(remoteProcessGroupElement, encryptor);
             final RemoteProcessGroup remoteGroup = controller.createRemoteProcessGroup(remoteGroupDto.getId(), remoteGroupDto.getTargetUris());
+            remoteGroup.setVersionedComponentId(remoteGroupDto.getVersionedComponentId());
             remoteGroup.setComments(remoteGroupDto.getComments());
             remoteGroup.setPosition(toPosition(remoteGroupDto.getPosition()));
             final String name = remoteGroupDto.getName();
@@ -1332,6 +1357,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
             }
 
             final Connection connection = controller.createConnection(dto.getId(), dto.getName(), source, destination, dto.getSelectedRelationships());
+            connection.setVersionedComponentId(dto.getVersionedComponentId());
             connection.setProcessGroup(processGroup);
 
             final List<Position> bendPoints = new ArrayList<>();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
index 88912aa..187b62f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
@@ -28,6 +28,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletableFuture;
@@ -126,6 +127,7 @@ public class StandardProcessorNode extends ProcessorNode implements Connectable
     private final AtomicInteger concurrentTaskCount;
     private final AtomicLong yieldExpiration;
     private final AtomicLong schedulingNanos;
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
     private final ProcessScheduler processScheduler;
     private long runNanos = 0L;
     private volatile long yieldNanos;
@@ -1511,4 +1513,26 @@ public class StandardProcessorNode extends ProcessorNode implements Connectable
         return group == null ? null : group.getIdentifier();
     }
 
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/label/StandardLabel.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/label/StandardLabel.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/label/StandardLabel.java
index 32e742d..2e98e84 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/label/StandardLabel.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/label/StandardLabel.java
@@ -28,6 +28,7 @@ import org.apache.nifi.util.CharacterFilterUtils;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class StandardLabel implements Label {
@@ -38,6 +39,7 @@ public class StandardLabel implements Label {
     private final AtomicReference<Map<String, String>> style;
     private final AtomicReference<String> value;
     private final AtomicReference<ProcessGroup> processGroup;
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
 
     public StandardLabel(final String identifier, final String value) {
         this(identifier, new Position(0D, 0D), new HashMap<String, String>(), value, null);
@@ -76,6 +78,7 @@ public class StandardLabel implements Label {
         }
     }
 
+    @Override
     public String getIdentifier() {
         return identifier;
     }
@@ -96,10 +99,12 @@ public class StandardLabel implements Label {
         return ResourceFactory.getComponentResource(ResourceType.Label, getIdentifier(),"Label");
     }
 
+    @Override
     public Map<String, String> getStyle() {
         return style.get();
     }
 
+    @Override
     public void setStyle(final Map<String, String> style) {
         if (style != null) {
             boolean updated = false;
@@ -112,19 +117,46 @@ public class StandardLabel implements Label {
         }
     }
 
+    @Override
     public String getValue() {
         return value.get();
     }
 
+    @Override
     public void setValue(final String value) {
         this.value.set(CharacterFilterUtils.filterInvalidXmlCharacters(value));
     }
 
+    @Override
     public void setProcessGroup(final ProcessGroup group) {
         this.processGroup.set(group);
     }
 
+    @Override
     public ProcessGroup getProcessGroup() {
         return processGroup.get();
     }
+
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
index f67ecd9..a2a589a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
@@ -50,6 +50,7 @@ import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
 
@@ -102,6 +103,7 @@ public class FlowFromDOMFactory {
         final ControllerServiceDTO dto = new ControllerServiceDTO();
 
         dto.setId(getString(element, "id"));
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
         dto.setName(getString(element, "name"));
         dto.setComments(getString(element, "comment"));
         dto.setType(getString(element, "class"));
@@ -138,6 +140,7 @@ public class FlowFromDOMFactory {
         final ProcessGroupDTO dto = new ProcessGroupDTO();
         final String groupId = getString(element, "id");
         dto.setId(groupId);
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
         dto.setParentGroupId(parentId);
         dto.setName(getString(element, "name"));
         dto.setPosition(getPosition(DomUtils.getChild(element, "position")));
@@ -153,6 +156,9 @@ public class FlowFromDOMFactory {
         }
         dto.setVariables(variables);
 
+        final Element versionControlInfoElement = DomUtils.getChild(element, "versionControlInformation");
+        dto.setVersionControlInformation(getVersionControlInformation(versionControlInfoElement));
+
         final Set<ProcessorDTO> processors = new HashSet<>();
         final Set<ConnectionDTO> connections = new HashSet<>();
         final Set<FunnelDTO> funnels = new HashSet<>();
@@ -216,12 +222,26 @@ public class FlowFromDOMFactory {
         return dto;
     }
 
+    private static VersionControlInformationDTO getVersionControlInformation(final Element versionControlInfoElement) {
+        if (versionControlInfoElement == null) {
+            return null;
+        }
+
+        final VersionControlInformationDTO dto = new VersionControlInformationDTO();
+        dto.setRegistryId(getString(versionControlInfoElement, "registryId"));
+        dto.setBucketId(getString(versionControlInfoElement, "bucketId"));
+        dto.setFlowId(getString(versionControlInfoElement, "flowId"));
+        dto.setVersion(getInt(versionControlInfoElement, "version"));
+        return dto;
+    }
+
     public static ConnectionDTO getConnection(final Element element) {
         final ConnectionDTO dto = new ConnectionDTO();
         dto.setId(getString(element, "id"));
         dto.setName(getString(element, "name"));
         dto.setLabelIndex(getOptionalInt(element, "labelIndex"));
         dto.setzIndex(getOptionalLong(element, "zIndex"));
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
 
         final List<PositionDTO> bends = new ArrayList<>();
         final Element bendPointsElement = DomUtils.getChild(element, "bendPoints");
@@ -278,6 +298,7 @@ public class FlowFromDOMFactory {
     public static RemoteProcessGroupDTO getRemoteProcessGroup(final Element element, final StringEncryptor encryptor) {
         final RemoteProcessGroupDTO dto = new RemoteProcessGroupDTO();
         dto.setId(getString(element, "id"));
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
         dto.setName(getString(element, "name"));
         dto.setTargetUri(getString(element, "url"));
         dto.setTargetUris(getString(element, "urls"));
@@ -302,6 +323,7 @@ public class FlowFromDOMFactory {
     public static LabelDTO getLabel(final Element element) {
         final LabelDTO dto = new LabelDTO();
         dto.setId(getString(element, "id"));
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
         dto.setLabel(getString(element, "value"));
         dto.setPosition(getPosition(DomUtils.getChild(element, "position")));
         final Size size = getSize(DomUtils.getChild(element, "size"));
@@ -315,6 +337,7 @@ public class FlowFromDOMFactory {
     public static FunnelDTO getFunnel(final Element element) {
         final FunnelDTO dto = new FunnelDTO();
         dto.setId(getString(element, "id"));
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
         dto.setPosition(getPosition(DomUtils.getChild(element, "position")));
 
         return dto;
@@ -323,6 +346,7 @@ public class FlowFromDOMFactory {
     public static PortDTO getPort(final Element element) {
         final PortDTO portDTO = new PortDTO();
         portDTO.setId(getString(element, "id"));
+        portDTO.setVersionedComponentId(getString(element, "versionedComponentId"));
         portDTO.setPosition(getPosition(DomUtils.getChild(element, "position")));
         portDTO.setName(getString(element, "name"));
         portDTO.setComments(getString(element, "comments"));
@@ -370,6 +394,7 @@ public class FlowFromDOMFactory {
         final String targetId = getString(element, "targetId");
         descriptor.setTargetId(targetId == null ? id : targetId);
 
+        descriptor.setVersionedComponentId(getString(element, "versionedComponentId"));
         descriptor.setName(getString(element, "name"));
         descriptor.setComments(getString(element, "comments"));
         descriptor.setConcurrentlySchedulableTaskCount(getInt(element, "maxConcurrentTasks"));
@@ -386,6 +411,7 @@ public class FlowFromDOMFactory {
         final ProcessorDTO dto = new ProcessorDTO();
 
         dto.setId(getString(element, "id"));
+        dto.setVersionedComponentId(getString(element, "versionedComponentId"));
         dto.setName(getString(element, "name"));
         dto.setType(getString(element, "class"));
         dto.setBundle(getBundle(DomUtils.getChild(element, "bundle")));

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
index bc28a25..ecf2438 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
@@ -39,6 +39,7 @@ import org.apache.nifi.persistence.TemplateSerializer;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.registry.VariableDescriptor;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.util.CharacterFilterUtils;
@@ -63,6 +64,7 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -151,10 +153,21 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement(elementName);
         parentElement.appendChild(element);
         addTextElement(element, "id", group.getIdentifier());
+        addTextElement(element, "versionedComponentId", group.getVersionedComponentId());
         addTextElement(element, "name", group.getName());
         addPosition(element, group.getPosition());
         addTextElement(element, "comment", group.getComments());
 
+        final VersionControlInformation versionControlInfo = group.getVersionControlInformation();
+        if (versionControlInfo != null) {
+            final Element versionControlInfoElement = doc.createElement("versionControlInformation");
+            addTextElement(versionControlInfoElement, "registryId", versionControlInfo.getRegistryIdentifier());
+            addTextElement(versionControlInfoElement, "bucketId", versionControlInfo.getBucketIdentifier());
+            addTextElement(versionControlInfoElement, "flowId", versionControlInfo.getFlowIdentifier());
+            addTextElement(versionControlInfoElement, "version", versionControlInfo.getVersion());
+            element.appendChild(versionControlInfoElement);
+        }
+
         for (final ProcessorNode processor : group.getProcessors()) {
             addProcessor(element, processor, scheduledStateLookup);
         }
@@ -258,6 +271,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement("label");
         parentElement.appendChild(element);
         addTextElement(element, "id", label.getIdentifier());
+        addTextElement(element, "versionedComponentId", label.getVersionedComponentId());
 
         addPosition(element, label.getPosition());
         addSize(element, label.getSize());
@@ -272,6 +286,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement("funnel");
         parentElement.appendChild(element);
         addTextElement(element, "id", funnel.getIdentifier());
+        addTextElement(element, "versionedComponentId", funnel.getVersionedComponentId());
         addPosition(element, funnel.getPosition());
     }
 
@@ -280,6 +295,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement("remoteProcessGroup");
         parentElement.appendChild(element);
         addTextElement(element, "id", remoteRef.getIdentifier());
+        addTextElement(element, "versionedComponentId", remoteRef.getVersionedComponentId());
         addTextElement(element, "name", remoteRef.getName());
         addPosition(element, remoteRef.getPosition());
         addTextElement(element, "comment", remoteRef.getComments());
@@ -322,6 +338,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement(elementName);
         parentElement.appendChild(element);
         addTextElement(element, "id", port.getIdentifier());
+        addTextElement(element, "versionedComponentId", port.getVersionedComponentId());
         addTextElement(element, "name", port.getName());
         addPosition(element, port.getPosition());
         addTextElement(element, "comments", port.getComments());
@@ -350,6 +367,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement(elementName);
         parentElement.appendChild(element);
         addTextElement(element, "id", port.getIdentifier());
+        addTextElement(element, "versionedComponentId", port.getVersionedComponentId());
         addTextElement(element, "name", port.getName());
         addPosition(element, port.getPosition());
         addTextElement(element, "comments", port.getComments());
@@ -363,6 +381,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement(elementName);
         parentElement.appendChild(element);
         addTextElement(element, "id", port.getIdentifier());
+        addTextElement(element, "versionedComponentId", port.getVersionedComponentId());
         addTextElement(element, "name", port.getName());
         addPosition(element, port.getPosition());
         addTextElement(element, "comments", port.getComments());
@@ -383,6 +402,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement("processor");
         parentElement.appendChild(element);
         addTextElement(element, "id", processor.getIdentifier());
+        addTextElement(element, "versionedComponentId", processor.getVersionedComponentId());
         addTextElement(element, "name", processor.getName());
 
         addPosition(element, processor.getPosition());
@@ -444,6 +464,7 @@ public class StandardFlowSerializer implements FlowSerializer {
         final Element element = doc.createElement("connection");
         parentElement.appendChild(element);
         addTextElement(element, "id", connection.getIdentifier());
+        addTextElement(element, "versionedComponentId", connection.getVersionedComponentId());
         addTextElement(element, "name", connection.getName());
 
         final Element bendPointsElement = doc.createElement("bendPoints");
@@ -500,6 +521,7 @@ public class StandardFlowSerializer implements FlowSerializer {
     public void addControllerService(final Element element, final ControllerServiceNode serviceNode) {
         final Element serviceElement = element.getOwnerDocument().createElement("controllerService");
         addTextElement(serviceElement, "id", serviceNode.getIdentifier());
+        addTextElement(serviceElement, "versionedComponentId", serviceNode.getVersionedComponentId());
         addTextElement(serviceElement, "name", serviceNode.getName());
         addTextElement(serviceElement, "comment", serviceNode.getComments());
         addTextElement(serviceElement, "class", serviceNode.getCanonicalClassName());
@@ -544,6 +566,17 @@ public class StandardFlowSerializer implements FlowSerializer {
         element.appendChild(toAdd);
     }
 
+    private static void addTextElement(final Element element, final String name, final Optional<String> value) {
+        if (!value.isPresent()) {
+            return;
+        }
+
+        final Document doc = element.getOwnerDocument();
+        final Element toAdd = doc.createElement(name);
+        toAdd.setTextContent(CharacterFilterUtils.filterInvalidXmlCharacters(value.get())); // value should already be filtered, but just in case ensure there are no invalid xml characters
+        element.appendChild(toAdd);
+    }
+
     public static void addTemplate(final Element element, final Template template) {
         try {
             final byte[] serialized = TemplateSerializer.serialize(template.getDetails());

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java
index 3faffd7..633f0ed 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java
@@ -198,6 +198,7 @@ public class ControllerServiceLoader {
         final ControllerServiceNode node = provider.createControllerService(dto.getType(), dto.getId(), coordinate, Collections.emptySet(), false);
         node.setName(dto.getName());
         node.setComments(dto.getComments());
+        node.setVersionedComponentId(dto.getVersionedComponentId());
         return node;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
index f46f796..53fd166 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
@@ -24,6 +24,7 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
@@ -72,6 +73,7 @@ public class StandardControllerServiceNode extends AbstractConfiguredComponent i
     private final AtomicReference<ControllerServiceDetails> controllerServiceHolder = new AtomicReference<>(null);
     private final ControllerServiceProvider serviceProvider;
     private final ServiceStateTransition stateTransition = new ServiceStateTransition();
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
 
     private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
     private final Lock readLock = rwLock.readLock();
@@ -526,4 +528,26 @@ public class StandardControllerServiceNode extends AbstractConfiguredComponent i
         return results != null ? results : Collections.emptySet();
     }
 
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
index 7c68475..3aa5084 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
@@ -268,6 +268,17 @@ public class FingerprintFactory {
     private StringBuilder addProcessGroupFingerprint(final StringBuilder builder, final Element processGroupElem, final FlowController controller) throws FingerprintException {
         // id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "id"));
+        appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "versionedComponentId"));
+
+        final Element versionControlInfo = DomUtils.getChild(processGroupElem, "versionControlInformation");
+        if (versionControlInfo == null) {
+            builder.append("NO_VERSION_CONTROL_INFORMATION");
+        } else {
+            appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "registryId"));
+            appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "bucketId"));
+            appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "flowId"));
+            appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "version"));
+        }
 
         // processors
         final List<Element> processorElems = DomUtils.getChildElementsByTagName(processGroupElem, "processor");
@@ -344,6 +355,7 @@ public class FingerprintFactory {
     private StringBuilder addFlowFileProcessorFingerprint(final StringBuilder builder, final Element processorElem) throws FingerprintException {
         // id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "id"));
+        appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "versionedComponentId"));
         // class
         final NodeList childNodes = DomUtils.getChildNodesByTagName(processorElem, "class");
         final String className = childNodes.item(0).getTextContent();
@@ -435,6 +447,7 @@ public class FingerprintFactory {
     private StringBuilder addPortFingerprint(final StringBuilder builder, final Element portElem) throws FingerprintException {
         // id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "id"));
+        appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "versionedComponentId"));
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "name"));
 
         final NodeList userAccessControlNodeList = DomUtils.getChildNodesByTagName(portElem, "userAccessControl");
@@ -471,13 +484,14 @@ public class FingerprintFactory {
 
     private StringBuilder addLabelFingerprint(final StringBuilder builder, final Element labelElem) {
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(labelElem, "id"));
+        appendFirstValue(builder, DomUtils.getChildNodesByTagName(labelElem, "versionedComponentId"));
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(labelElem, "value"));
         return builder;
     }
 
     private StringBuilder addRemoteProcessGroupFingerprint(final StringBuilder builder, final Element remoteProcessGroupElem) throws FingerprintException {
 
-        for (String tagName : new String[]{"id", "urls", "networkInterface", "timeout", "yieldPeriod",
+        for (String tagName : new String[] {"id", "versionedComponentId", "urls", "networkInterface", "timeout", "yieldPeriod",
                 "transportProtocol", "proxyHost", "proxyPort", "proxyUser", "proxyPassword"}) {
             final String value = getFirstValue(DomUtils.getChildNodesByTagName(remoteProcessGroupElem, tagName));
             if (isEncrypted(value)) {
@@ -544,7 +558,7 @@ public class FingerprintFactory {
     }
 
     private StringBuilder addRemoteGroupPortFingerprint(final StringBuilder builder, final Element remoteGroupPortElement) {
-        for (final String childName : new String[] {"id", "targetId", "maxConcurrentTasks", "useCompression", "batchCount", "batchSize", "batchDuration"}) {
+        for (final String childName : new String[] {"id", "targetId", "versionedComponentId", "maxConcurrentTasks", "useCompression", "batchCount", "batchSize", "batchDuration"}) {
             appendFirstValue(builder, DomUtils.getChildNodesByTagName(remoteGroupPortElement, childName));
         }
 
@@ -555,6 +569,7 @@ public class FingerprintFactory {
     private StringBuilder addConnectionFingerprint(final StringBuilder builder, final Element connectionElem) throws FingerprintException {
         // id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "id"));
+        appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "versionedComponentId"));
         // source id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "sourceId"));
         // source group id
@@ -583,11 +598,13 @@ public class FingerprintFactory {
     private StringBuilder addFunnelFingerprint(final StringBuilder builder, final Element funnelElem) throws FingerprintException {
         // id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(funnelElem, "id"));
+        appendFirstValue(builder, DomUtils.getChildNodesByTagName(funnelElem, "versionedComponentId"));
         return builder;
     }
 
     private void addControllerServiceFingerprint(final StringBuilder builder, final ControllerServiceDTO dto) {
         builder.append(dto.getId());
+        builder.append(dto.getVersionedComponentId());
         builder.append(dto.getType());
         builder.append(dto.getName());
 


[43/50] nifi git commit: NIFI-4436: - Minor UX tweaks. - Updating imports due to refactoring of Registry models. - Fixing checkstyle issues.

Posted by bb...@apache.org.
NIFI-4436:
- Minor UX tweaks.
- Updating imports due to refactoring of Registry models.
- Fixing checkstyle issues.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/c5b0931e
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/c5b0931e
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/c5b0931e

Branch: refs/heads/master
Commit: c5b0931e558f7043b29ae1d2edc38dbd5607d289
Parents: 416b861
Author: Matt Gilman <ma...@gmail.com>
Authored: Fri Dec 15 09:49:13 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:56 2018 -0500

----------------------------------------------------------------------
 .../main/java/org/apache/nifi/web/NiFiServiceFacade.java    | 6 +++---
 .../java/org/apache/nifi/web/StandardNiFiServiceFacade.java | 2 +-
 .../WEB-INF/partials/canvas/import-flow-version-dialog.jsp  | 2 +-
 .../WEB-INF/partials/canvas/save-flow-version-dialog.jsp    | 8 ++++----
 .../nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css     | 2 +-
 .../nifi-web-ui/src/main/webapp/css/flow-status.css         | 4 ++++
 .../nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js   | 9 ---------
 .../src/main/webapp/js/nf/canvas/nf-context-menu.js         | 4 +++-
 .../main/webapp/views/nf-ng-breadcrumbs-directive-view.html | 2 +-
 9 files changed, 18 insertions(+), 21 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 6c20eac..ab96747 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -1289,7 +1289,7 @@ public interface NiFiServiceFacade {
      * Returns a FlowComparisonEntity that contains all of the local modifications since the Process Group
      * was last synchronized with the Flow Registry
      *
-     * @param processGroupId
+     * @param processGroupId the ID of the Process Group
      * @return a FlowComparisonEntity that contains all of the local modifications since the Process Group
      *         was last synchronized with the Flow Registry
      * @throws IllegalStateException if the Process Group with the given ID is not under version control
@@ -1455,8 +1455,8 @@ public interface NiFiServiceFacade {
      *            update the contents of that Process Group
      * @return the Process Group
      */
-    ProcessGroupEntity updateProcessGroupContents(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
-        boolean verifyNotModified, boolean updateSettings, boolean updateDescendantVersionedFlows);
+    ProcessGroupEntity updateProcessGroupContents(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot,
+                                                  String componentIdSeed, boolean verifyNotModified, boolean updateSettings, boolean updateDescendantVersionedFlows);
 
     // ----------------------------------------
     // Component state methods

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 1da63c2..1865bff 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -87,6 +87,7 @@ import org.apache.nifi.history.History;
 import org.apache.nifi.history.HistoryQuery;
 import org.apache.nifi.history.PreviousValue;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.authorization.Permissions;
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.FlowRegistry;
@@ -115,7 +116,6 @@ import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
-import org.apache.nifi.registry.model.authorization.Permissions;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.reporting.Bulletin;

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
index 5ea622a..01e03d5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
@@ -25,7 +25,7 @@
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Location</div>
+            <div class="setting-name">Bucket</div>
             <div class="setting-field">
                 <div id="import-flow-version-bucket-combo"></div>
                 <div id="import-flow-version-bucket" class="hidden"></div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
index a7f54b6..6017f37 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
@@ -19,14 +19,13 @@
     <div class="dialog-content">
         <div class="setting">
             <div class="setting-name">Registry</div>
-            <div id="save-flow-version-registry-container" class="setting-field">
+            <div class="setting-field">
                 <div id="save-flow-version-registry-combo" class="hidden"></div>
                 <div id="save-flow-version-registry" class="hidden"></div>
-                <div id="save-flow-version-label"></div>
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Location</div>
+            <div class="setting-name">Bucket</div>
             <div class="setting-field">
                 <div id="save-flow-version-bucket-combo" class="hidden"></div>
                 <div id="save-flow-version-bucket" class="hidden"></div>
@@ -34,10 +33,11 @@
         </div>
         <div class="setting">
             <div class="setting-name">Name</div>
-            <div class="setting-field">
+            <div id="save-flow-version-registry-container" class="setting-field">
                 <span id="save-flow-version-process-group-id" class="hidden"></span>
                 <input type="text" id="save-flow-version-name-field" class="setting-input hidden"/>
                 <div id="save-flow-version-name" class="hidden"></div>
+                <div id="save-flow-version-label"></div>
             </div>
         </div>
         <div class="setting">

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index 710c0fb..82ffc38 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -224,7 +224,7 @@ div.progress-label {
     height: 85px;
 }
 
-#save-flow-version-registry-combo, #save-flow-version-registry {
+#save-flow-version-name-field, #save-flow-version-name {
     width: 100%;
     margin-right: 10px;
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/flow-status.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/flow-status.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/flow-status.css
index 42f9afd..35ef016 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/flow-status.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/flow-status.css
@@ -67,6 +67,10 @@
     padding-left: 5px;
 }
 
+#flow-status-container {
+    width: 90%;
+}
+
 #search-container {
     overflow: hidden;
     height: 32px;

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
index 692dd58..00dd4a8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
@@ -609,14 +609,6 @@
                 });
             };
 
-            // define a function for update the flow status dimensions
-            var updateFlowStatusContainerSize = function () {
-                $('#flow-status-container').css({
-                    'width': ((($('#nifi-logo').width() + $('#component-container').width())/$(window).width())*100)*2 + '%'
-                });
-            };
-            updateFlowStatusContainerSize();
-
             // listen for events to go to components
             $('body').on('GoTo:Component', function (e, item) {
                 nfCanvasUtils.showComponent(item.parentGroupId, item.id);
@@ -641,7 +633,6 @@
                     }
 
                     updateGraphSize();
-                    updateFlowStatusContainerSize();
 
                     // resize shell when appropriate
                     var shell = $('#shell-dialog');

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index 0c21838..5571e08 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -541,7 +541,9 @@
         }
 
         // check the selection for version control information
-        return versionControlInformation.state !== 'LOCALLY_MODIFIED' && versionControlInformation.state !== 'LOCALLY_MODIFIED_AND_STALE';
+        return versionControlInformation.state !== 'LOCALLY_MODIFIED' &&
+            versionControlInformation.state !== 'LOCALLY_MODIFIED_AND_STALE' &&
+            versionControlInformation.state !== 'SYNC_FAILURE';
     };
 
     /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/c5b0931e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
index a69de46..19018d3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
@@ -22,7 +22,7 @@ limitations under the License.
                 <span ng-if="separatorFunc(crumb.parentBreadcrumb)" style="margin: 0 12px;">
                     &raquo;
                 </span>
-                <span ng-if="isTracking(crumb)" title="{{getVersionControlTooltip(crumb)}}" class="{{getVersionControlClass(crumb)}}" style="margin: 0 6px;"></span>
+                <span ng-if="isTracking(crumb)" title="{{getVersionControlTooltip(crumb)}}" class="{{getVersionControlClass(crumb)}}" style="margin-right: 6px;"></span>
                 <span class="link"
                       ng-class="(highlightCrumbId === crumb.id) ? 'link-bold' : ''"
                       ng-click="clickFunc(crumb.id)">


[30/50] nifi git commit: NIFI-4436: - Fixing default border radius. - Code clean up. - Ensuring component visibility is updated after updating/reverting. - Fixing sort on component name in local changes dialog NIFI-4526: - Added front end controls for up

Posted by bb...@apache.org.
NIFI-4436:
- Fixing default border radius.
- Code clean up.
- Ensuring component visibility is updated after updating/reverting.
- Fixing sort on component name in local changes dialog
NIFI-4526:
- Added front end controls for updating RPG target URL.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/c92022dd
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/c92022dd
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/c92022dd

Branch: refs/heads/master
Commit: c92022dd60e599ef6ed176d31489ba265acedd48
Parents: adacb20
Author: Matt Gilman <ma...@gmail.com>
Authored: Mon Nov 27 16:10:39 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:54 2018 -0500

----------------------------------------------------------------------
 .../nifi/remote/StandardRemoteProcessGroup.java |   6 ++
 .../remote-process-group-configuration.jsp      |   2 +-
 .../src/main/webapp/css/common-ui.css           |   4 +
 .../main/webapp/js/nf/canvas/nf-canvas-utils.js |   2 +-
 .../src/main/webapp/js/nf/canvas/nf-canvas.js   | 105 +------------------
 .../main/webapp/js/nf/canvas/nf-flow-version.js |  17 ++-
 .../nf-remote-process-group-configuration.js    |   5 +-
 7 files changed, 31 insertions(+), 110 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index 6b55735..726daa0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -192,6 +192,12 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
     @Override
     public void setTargetUris(final String targetUris) {
         requireNonNull(targetUris);
+
+        // only attempt to update the target uris if they have changed
+        if (targetUris.equals(this.targetUris)) {
+            return;
+        }
+
         verifyCanUpdate();
 
         this.targetUris = targetUris;

http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/remote-process-group-configuration.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/remote-process-group-configuration.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/remote-process-group-configuration.jsp
index 36ecd48..dac6222 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/remote-process-group-configuration.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/remote-process-group-configuration.jsp
@@ -32,7 +32,7 @@
         <div class="setting">
             <div class="setting-name">URLs</div>
             <div class="setting-field">
-                <span id="remote-process-group-urls"></span>
+                <input type="text" id="remote-process-group-urls" class="setting-input"/>
             </div>
         </div>
         <div class="setting">

http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
index e271ff4..ac4cb62 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css
@@ -109,6 +109,10 @@ body {
     color: #262626;
 }
 
+button {
+    border-radius: 0;
+}
+
 .value-color {
     color: #775351;
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
index 213b572..1aefa44 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
@@ -543,7 +543,7 @@
                             params.set('processGroupId', 'root');
                         }
 
-                        if ((url.origin + url.pathname + '?' + params.toString()).length <= nfCanvasUtils.MAX_URL_LENGTH) {
+                        if ((url.origin + url.pathname + '?' + params.toString()).length <= MAX_URL_LENGTH) {
                             newUrl = url.origin + url.pathname + '?' + params.toString();
                         } else if (nfCommon.isDefinedAndNotNull(nfCanvasUtils.getParentGroupId())) {
                             // silently remove all component ids

http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
index 49ded8c..692dd58 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
@@ -182,7 +182,7 @@
             }, options));
 
             // update component visibility
-            nfCanvas.View.updateVisibility();
+            nfGraph.updateVisibility();
 
             // update the birdseye
             nfBirdseye.refresh();
@@ -979,97 +979,6 @@
 
         View: (function () {
 
-            /**
-             * Updates component visibility based on their proximity to the screen's viewport.
-             */
-            var updateComponentVisibility = function () {
-                var canvasContainer = $('#canvas-container');
-                var translate = nfCanvas.View.translate();
-                var scale = nfCanvas.View.scale();
-
-                // scale the translation
-                translate = [translate[0] / scale, translate[1] / scale];
-
-                // get the normalized screen width and height
-                var screenWidth = canvasContainer.width() / scale;
-                var screenHeight = canvasContainer.height() / scale;
-
-                // calculate the screen bounds one screens worth in each direction
-                var screenLeft = -translate[0] - screenWidth;
-                var screenTop = -translate[1] - screenHeight;
-                var screenRight = screenLeft + (screenWidth * 3);
-                var screenBottom = screenTop + (screenHeight * 3);
-
-                // detects whether a component is visible and should be rendered
-                var isComponentVisible = function (d) {
-                    if (!nfCanvas.View.shouldRenderPerScale()) {
-                        return false;
-                    }
-
-                    var left = d.position.x;
-                    var top = d.position.y;
-                    var right = left + d.dimensions.width;
-                    var bottom = top + d.dimensions.height;
-
-                    // determine if the component is now visible
-                    return screenLeft < right && screenRight > left && screenTop < bottom && screenBottom > top;
-                };
-
-                // detects whether a connection is visible and should be rendered
-                var isConnectionVisible = function (d) {
-                    if (!nfCanvas.View.shouldRenderPerScale()) {
-                        return false;
-                    }
-
-                    var x, y;
-                    if (d.bends.length > 0) {
-                        var i = Math.min(Math.max(0, d.labelIndex), d.bends.length - 1);
-                        x = d.bends[i].x;
-                        y = d.bends[i].y;
-                    } else {
-                        x = (d.start.x + d.end.x) / 2;
-                        y = (d.start.y + d.end.y) / 2;
-                    }
-
-                    return screenLeft < x && screenRight > x && screenTop < y && screenBottom > y;
-                };
-
-                // marks the specific component as visible and determines if its entering or leaving visibility
-                var updateVisibility = function (d, isVisible) {
-                    var selection = d3.select('#id-' + d.id);
-                    var visible = isVisible(d);
-                    var wasVisible = selection.classed('visible');
-
-                    // mark the selection as appropriate
-                    selection.classed('visible', visible)
-                        .classed('entering', function () {
-                            return visible && !wasVisible;
-                        }).classed('leaving', function () {
-                        return !visible && wasVisible;
-                    });
-                };
-
-                // get the all components
-                var graph = nfGraph.get();
-
-                // update the visibility for each component
-                $.each(graph.processors, function (_, d) {
-                    updateVisibility(d, isComponentVisible);
-                });
-                $.each(graph.ports, function (_, d) {
-                    updateVisibility(d, isComponentVisible);
-                });
-                $.each(graph.processGroups, function (_, d) {
-                    updateVisibility(d, isComponentVisible);
-                });
-                $.each(graph.remoteProcessGroups, function (_, d) {
-                    updateVisibility(d, isComponentVisible);
-                });
-                $.each(graph.connections, function (_, d) {
-                    updateVisibility(d, isConnectionVisible);
-                });
-            };
-
             // initialize the zoom behavior
             var behavior;
 
@@ -1112,7 +1021,7 @@
                         .on('zoomend', function () {
                             // ensure the canvas was actually refreshed
                             if (nfCommon.isDefinedAndNotNull(refreshed)) {
-                                nfCanvas.View.updateVisibility();
+                                nfGraph.updateVisibility();
 
                                 // refresh the birdseye
                                 refreshed.done(function () {
@@ -1144,14 +1053,6 @@
                 },
 
                 /**
-                 * Updates component visibility based on the current translation/scale.
-                 */
-                updateVisibility: function () {
-                    updateComponentVisibility();
-                    nfGraph.pan();
-                },
-
-                /**
                  * Sets/gets the current translation.
                  *
                  * @param {array} translate     [x, y]
@@ -1346,7 +1247,7 @@
 
                         // update component visibility
                         if (refreshComponents) {
-                            nfCanvas.View.updateVisibility();
+                            nfGraph.updateVisibility();
                         }
 
                         // persist if appropriate

http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 28e4303..cb42c30 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -541,11 +541,16 @@
 
             // determine if the item matches the filter
             var matchesId = item['componentId'].search(filterExp) >= 0;
-            var matchesComponent = item['componentName'].search(filterExp) >= 0;
             var matchesDifferenceType = item['differenceType'].search(filterExp) >= 0;
             var matchesDifference = item['difference'].search(filterExp) >= 0;
 
-            return matchesId || matchesComponent || matchesDifferenceType || matchesDifference;
+            // conditionally consider the component name
+            var matchesComponentName = false;
+            if (nfCommon.isDefinedAndNotNull(item['componentName'])) {
+                matchesComponentName = item['componentName'].search(filterExp) >= 0;
+            }
+
+            return matchesId || matchesComponentName || matchesDifferenceType || matchesDifference;
         };
 
         // initialize the component state filter
@@ -570,7 +575,7 @@
         // define the column model for local changes
         var localChangesColumns = [
             {
-                id: 'component',
+                id: 'componentName',
                 name: 'Component Name',
                 field: 'componentName',
                 formatter: valueFormatter,
@@ -610,7 +615,7 @@
         });
         localChangesData.setFilterArgs({
             searchString: '',
-            property: 'component'
+            property: 'componentName'
         });
         localChangesData.setFilter(filter);
 
@@ -1202,7 +1207,11 @@
                 url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId),
                 dataType: 'json'
             }).done(function (response) {
+                // update the graph components
                 nfGraph.set(response.processGroupFlow.flow);
+
+                // update the component visibility
+                nfGraph.updateVisibility();
             }).fail(nfErrorHandler.handleAjaxError);
         } else {
             // if reverting selected PG... reload selected PG to update counts, etc

http://git-wip-us.apache.org/repos/asf/nifi/blob/c92022dd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group-configuration.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group-configuration.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group-configuration.js
index 2712713..279350e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group-configuration.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group-configuration.js
@@ -78,6 +78,7 @@
                                 'revision': nfClient.getRevision(remoteProcessGroupData),
                                 'component': {
                                     id: remoteProcessGroupId,
+                                    targetUris: $('#remote-process-group-urls').val(),
                                     communicationsTimeout: $('#remote-process-group-timeout').val(),
                                     yieldDuration: $('#remote-process-group-yield-duration').val(),
                                     transportProtocol: $('#remote-process-group-transport-protocol-combo').combo('getSelectedOption').value,
@@ -145,7 +146,7 @@
                         // clear the remote process group details
                         $('#remote-process-group-id').text('');
                         $('#remote-process-group-name').text('');
-                        $('#remote-process-group-urls').text('');
+                        $('#remote-process-group-urls').val('');
                         $('#remote-process-group-timeout').val('');
                         $('#remote-process-group-yield-duration').val('');
                         $('#remote-process-group-transport-protocol-combo').combo('setSelectedOption', {
@@ -184,7 +185,7 @@
                 // populate the port settings
                 $('#remote-process-group-id').text(selectionData.id);
                 $('#remote-process-group-name').text(selectionData.component.name);
-                $('#remote-process-group-urls').text(selectionData.component.targetUris);
+                $('#remote-process-group-urls').val(selectionData.component.targetUris);
 
                 // populate the text fields
                 $('#remote-process-group-timeout').val(selectionData.component.communicationsTimeout);


[20/50] nifi git commit: NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated autho

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index f61b399..3684f04 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,12 +17,37 @@
 
 package org.apache.nifi.web.api;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.ProcessGroupAuthorizable;
@@ -34,7 +59,9 @@ import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.flow.FlowRegistryUtils;
+import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
@@ -69,35 +96,12 @@ import org.apache.nifi.web.util.Pause;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 
 @Path("/versions")
 @Api(value = "/versions", description = "Endpoint for managing version control for a flow")
@@ -125,9 +129,12 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
-    @ApiOperation(value = "Gets the Version Control information for a process group", response = VersionControlInformationEntity.class, notes = NON_GUARANTEED_ENDPOINT, authorizations = {
-        @Authorization(value = "Read - /process-groups/{uuid}")
-    })
+    @ApiOperation(value = "Gets the Version Control information for a process group",
+        response = VersionControlInformationEntity.class,
+        notes = NON_GUARANTEED_ENDPOINT,
+        authorizations = {
+            @Authorization(value = "Read - /process-groups/{uuid}")
+        })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -164,7 +171,9 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("start-requests")
     @ApiOperation(
-            value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed",
+        value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed. Creating this request will "
+            + "prevent any other threads from simultaneously saving local changes to Version Control. It will not, however, actually save the local flow to the Flow Registry. A "
+            + "POST to /versions/process-groups/{id} should be used to initiate saving of the local flow to the Flow Registry.",
             response = VersionControlInformationEntity.class,
             notes = NON_GUARANTEED_ENDPOINT)
     @ApiResponses(value = {
@@ -305,7 +314,8 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("start-requests/{id}")
     @ApiOperation(
-            value = "Deletes the request with the given ID",
+        value = "Deletes the Version Control Request with the given ID. This will allow other threads to save flows to the Flow Registry. See also the documentation "
+            + "for POSTing to /versions/start-requests for information regarding why this is done.",
             notes = NON_GUARANTEED_ENDPOINT)
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -349,7 +359,8 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
     @ApiOperation(
-            value = "Begins version controlling the Process Group with the given ID",
+            value = "Begins version controlling the Process Group with the given ID or commits changes to the Versioned Flow, "
+                + "depending on if the provided VersionControlInformation includes a flowId",
             response = VersionControlInformationEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -365,7 +376,7 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 404, message = "The specified resource could not be found."),
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
-    public Response startVersionControl(
+    public Response saveToFlowRegistry(
         @ApiParam("The process group id.") @PathParam("id") final String groupId,
         @ApiParam(value = "The versioned flow details.", required = true) final StartVersionControlRequestEntity requestEntity) throws IOException {
 
@@ -402,29 +413,7 @@ public class VersionsResource extends ApplicationResource {
             final URI requestUri;
             try {
                 final URI originalUri = getAbsolutePath();
-                final URI createRequestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
-                    originalUri.getPort(), "/nifi-api/versions/start-requests", null, originalUri.getFragment());
-
-                final NodeResponse clusterResponse;
-                try {
-                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                        clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
-                    } else {
-                        clusterResponse = getRequestReplicator().forwardToCoordinator(
-                            getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
-                    }
-                } catch (final InterruptedException ie) {
-                    Thread.currentThread().interrupt();
-                    throw new RuntimeException("Interrupted while updating Version Control Information for Process Group with ID " + groupId + ".", ie);
-                }
-
-                if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
-                    final String errorResponse = getResponseEntity(clusterResponse, String.class);
-                    throw new IllegalStateException(
-                        "Failed to create a Version Control Request across all nodes in the cluster. Received response code " + clusterResponse.getStatus() + " with content: " + errorResponse);
-                }
-
-                final String requestId = getResponseEntity(clusterResponse, String.class);
+                final String requestId = lockVersionControl(originalUri, groupId);
 
                 requestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
                     originalUri.getPort(), "/nifi-api/versions/start-requests/" + requestId, null, originalUri.getFragment());
@@ -439,54 +428,12 @@ public class VersionsResource extends ApplicationResource {
             // Finally, we can delete the Request.
             try {
                 final VersionControlComponentMappingEntity mappingEntity = serviceFacade.registerFlowWithFlowRegistry(groupId, requestEntity);
-
-                final Map<String, String> headers = new HashMap<>();
-                headers.put("content-type", MediaType.APPLICATION_JSON);
-
-                final NodeResponse clusterResponse;
-                try {
-                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                        clusterResponse = getRequestReplicator().replicate(HttpMethod.PUT, requestUri, mappingEntity, headers).awaitMergedResponse();
-                    } else {
-                        clusterResponse = getRequestReplicator().forwardToCoordinator(
-                            getClusterCoordinatorNode(), HttpMethod.PUT, requestUri, mappingEntity, headers).awaitMergedResponse();
-                    }
-                } catch (final InterruptedException ie) {
-                    Thread.currentThread().interrupt();
-                    throw new RuntimeException("Interrupted while updating Version Control Information for Process Group with ID " + groupId + ".", ie);
-                }
-
-                if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
-                    final String message = "Failed to update Version Control Information for Process Group with ID " + groupId + ".";
-                    final Throwable cause = clusterResponse.getThrowable();
-                    if (cause == null) {
-                        throw new IllegalStateException(message);
-                    } else {
-                        throw new IllegalStateException(message, cause);
-                    }
-                }
+                replicateVersionControlMapping(mappingEntity, requestEntity, requestUri, groupId);
 
                 final VersionControlInformationEntity responseEntity = serviceFacade.getVersionControlInformation(groupId);
                 return generateOkResponse(responseEntity).build();
             } finally {
-                final NodeResponse clusterResponse;
-                try {
-                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                        clusterResponse = getRequestReplicator().replicate(HttpMethod.DELETE, requestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
-                    } else {
-                        clusterResponse = getRequestReplicator().forwardToCoordinator(
-                            getClusterCoordinatorNode(), HttpMethod.DELETE, requestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
-                    }
-                } catch (final InterruptedException ie) {
-                    Thread.currentThread().interrupt();
-                    throw new RuntimeException("After starting Version Control on Process Group with ID " + groupId + ", interrupted while waiting for deletion of Version Control Request. "
-                        + "Users may be unable to Version Control other Process Groups until the request lock times out.", ie);
-                }
-
-                if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
-                    logger.error("After starting Version Control on Process Group with ID " + groupId + ", failed to delete Version Control Request. "
-                        + "Users may be unable to Version Control other Process Groups until the request lock times out. Response status code was " + clusterResponse.getStatus());
-                }
+                unlockVersionControl(requestUri, groupId);
             }
         }
 
@@ -560,13 +507,115 @@ public class VersionsResource extends ApplicationResource {
             });
     }
 
+    private void unlockVersionControl(final URI requestUri, final String groupId) {
+        final NodeResponse clusterResponse;
+        try {
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.DELETE, requestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), HttpMethod.DELETE, requestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
+            }
+        } catch (final InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("After starting Version Control on Process Group with ID " + groupId + ", interrupted while waiting for deletion of Version Control Request. "
+                + "Users may be unable to Version Control other Process Groups until the request lock times out.", ie);
+        }
+
+        if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+            logger.error("After starting Version Control on Process Group with ID " + groupId + ", failed to delete Version Control Request. "
+                + "Users may be unable to Version Control other Process Groups until the request lock times out. Response status code was " + clusterResponse.getStatus());
+        }
+    }
+
+    private String lockVersionControl(final URI originalUri, final String groupId) throws URISyntaxException {
+        final URI createRequestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+            originalUri.getPort(), "/nifi-api/versions/start-requests", null, originalUri.getFragment());
+
+        final NodeResponse clusterResponse;
+        try {
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
+            }
+        } catch (final InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("Interrupted while updating Version Control Information for Process Group with ID " + groupId + ".", ie);
+        }
+
+        if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+            final String errorResponse = getResponseEntity(clusterResponse, String.class);
+            throw new IllegalStateException(
+                "Failed to create a Version Control Request across all nodes in the cluster. Received response code " + clusterResponse.getStatus() + " with content: " + errorResponse);
+        }
+
+        final String requestId = getResponseEntity(clusterResponse, String.class);
+        return requestId;
+    }
+
+    private void replicateVersionControlMapping(final VersionControlComponentMappingEntity mappingEntity, final StartVersionControlRequestEntity requestEntity, final URI requestUri,
+        final String groupId) {
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("content-type", MediaType.APPLICATION_JSON);
+
+        final NodeResponse clusterResponse;
+        try {
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.PUT, requestUri, mappingEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), HttpMethod.PUT, requestUri, mappingEntity, headers).awaitMergedResponse();
+            }
+        } catch (final InterruptedException ie) {
+            Thread.currentThread().interrupt();
+
+            if (requestEntity.getVersionedFlow().getFlowId() == null) {
+                // We had to create the flow for this snapshot. Since we failed to replicate the Version Control Info, remove the
+                // flow from the Flow Registry (use best effort; if we can't remove it, just log and move on).
+                final VersionControlInformationDTO vci = mappingEntity.getVersionControlInformation();
+                try {
+                    serviceFacade.deleteVersionedFlow(vci.getRegistryId(), vci.getBucketId(), vci.getFlowId());
+                } catch (final Exception e) {
+                    logger.error("Created Versioned Flow with ID {} in bucket with ID {} but failed to replicate the Version Control Information to cluster. "
+                        + "Attempted to delete the newly created (empty) flow from the Flow Registry but failed", vci.getFlowId(), vci.getBucketId(), e);
+                }
+            }
+
+            throw new RuntimeException("Interrupted while updating Version Control Information for Process Group with ID " + groupId + ".", ie);
+        }
+
+        if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+            if (requestEntity.getVersionedFlow().getFlowId() == null) {
+                // We had to create the flow for this snapshot. Since we failed to replicate the Version Control Info, remove the
+                // flow from the Flow Registry (use best effort; if we can't remove it, just log and move on).
+                final VersionControlInformationDTO vci = mappingEntity.getVersionControlInformation();
+                try {
+                    serviceFacade.deleteVersionedFlow(vci.getRegistryId(), vci.getBucketId(), vci.getFlowId());
+                } catch (final Exception e) {
+                    logger.error("Created Versioned Flow with ID {} in bucket with ID {} but failed to replicate the Version Control Information to cluster. "
+                        + "Attempted to delete the newly created (empty) flow from the Flow Registry but failed", vci.getFlowId(), vci.getBucketId(), e);
+                }
+            }
+
+            final String message = "Failed to update Version Control Information for Process Group with ID " + groupId + ".";
+            final Throwable cause = clusterResponse.getThrowable();
+            if (cause == null) {
+                throw new IllegalStateException(message);
+            } else {
+                throw new IllegalStateException(message, cause);
+            }
+        }
+    }
+
 
     @DELETE
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
     @ApiOperation(
-            value = "Stops version controlling the Process Group with the given ID",
+            value = "Stops version controlling the Process Group with the given ID. The Process Group will no longer track to any Versioned Flow.",
             response = VersionControlInformationEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -626,7 +675,8 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
     @ApiOperation(
-            value = "For a Process Group that is already under Version Control, this will update the version of the flow to a different version",
+            value = "For a Process Group that is already under Version Control, this will update the version of the flow to a different version. This endpoint expects "
+                + "that the given snapshot will not modify any Processor that is currently running or any Controller Service that is enabled.",
             response = VersionControlInformationEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -689,14 +739,17 @@ public class VersionsResource extends ApplicationResource {
                 serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, false);
             },
             (rev, entity) -> {
+                final Bucket bucket = flowSnapshot.getBucket();
+                final VersionedFlow flow = flowSnapshot.getFlow();
+
                 // Update the Process Group to match the proposed flow snapshot
                 final VersionControlInformationDTO versionControlInfoDto = new VersionControlInformationDTO();
                 versionControlInfoDto.setBucketId(snapshotMetadata.getBucketIdentifier());
-                versionControlInfoDto.setBucketName(snapshotMetadata.getBucketName());
+                versionControlInfoDto.setBucketName(bucket.getName());
                 versionControlInfoDto.setCurrent(true);
                 versionControlInfoDto.setFlowId(snapshotMetadata.getFlowIdentifier());
-                versionControlInfoDto.setFlowName(snapshotMetadata.getFlowName());
-                versionControlInfoDto.setFlowDescription(snapshotMetadata.getFlowDescription());
+                versionControlInfoDto.setFlowName(flow.getName());
+                versionControlInfoDto.setFlowDescription(flow.getDescription());
                 versionControlInfoDto.setGroupId(groupId);
                 versionControlInfoDto.setModified(false);
                 versionControlInfoDto.setVersion(snapshotMetadata.getVersion());
@@ -720,7 +773,9 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("update-requests/{id}")
     @ApiOperation(
-            value = "Returns the Update Request with the given ID",
+            value = "Returns the Update Request with the given ID. Once an Update Request has been created by performing a POST to /versions/update-requests/process-groups/{id}, "
+                + "that request can subsequently be retrieved via this endpoint, and the request that is fetched will contain the updated state, such as percent complete, the "
+                + "current state of the request, and any failures.",
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -741,7 +796,9 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("revert-requests/{id}")
     @ApiOperation(
-            value = "Returns the Revert Request with the given ID",
+            value = "Returns the Revert Request with the given ID. Once a Revert Request has been created by performing a POST to /versions/revert-requests/process-groups/{id}, "
+                + "that request can subsequently be retrieved via this endpoint, and the request that is fetched will contain the updated state, such as percent complete, the "
+                + "current state of the request, and any failures.",
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -795,7 +852,9 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("update-requests/{id}")
     @ApiOperation(
-            value = "Deletes the Update Request with the given ID",
+        value = "Deletes the Update Request with the given ID. After a request is created via a POST to /versions/update-requests/process-groups/{id}, it is expected "
+            + "that the client will properly clean up the request by DELETE'ing it, once the Update process has completed. If the request is deleted before the request "
+            + "completes, then the Update request will finish the step that it is currently performing and then will cancel any subsequent steps.",
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -816,7 +875,9 @@ public class VersionsResource extends ApplicationResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("revert-requests/{id}")
     @ApiOperation(
-            value = "Deletes the Revert Request with the given ID",
+            value = "Deletes the Revert Request with the given ID. After a request is created via a POST to /versions/revert-requests/process-groups/{id}, it is expected "
+                + "that the client will properly clean up the request by DELETE'ing it, once the Revert process has completed. If the request is deleted before the request "
+            + "completes, then the Revert request will finish the step that it is currently performing and then will cancel any subsequent steps.",
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -881,7 +942,12 @@ public class VersionsResource extends ApplicationResource {
     @Path("update-requests/process-groups/{id}")
     @ApiOperation(
             value = "For a Process Group that is already under Version Control, this will initiate the action of changing "
-                    + "from a specific version of the flow in the Flow Registry to a different version of the flow.",
+                + "from a specific version of the flow in the Flow Registry to a different version of the flow. This can be a lengthy "
+                + "process, as it will stop any Processors and disable any Controller Services necessary to perform the action and then restart them. As a result, "
+                + "the endpoint will immediately return a VersionedFlowUpdateRequestEntity, and the process of updating the flow will occur "
+                + "asynchronously in the background. The client may then periodically poll the status of the request by issuing a GET request to "
+                + "/versions/update-requests/{requestId}. Once the request is completed, the client is expected to issue a DELETE request to "
+                + "/versions/update-requests/{requestId}.",
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -1058,8 +1124,13 @@ public class VersionsResource extends ApplicationResource {
     @Path("revert-requests/process-groups/{id}")
     @ApiOperation(
             value = "For a Process Group that is already under Version Control, this will initiate the action of reverting "
-                + "any changes that have been made to the Process Group since it was last synchronized with the Flow Registry. This will result in the "
-                + "flow matching the Versioned Flow that exists in the Flow Registry.",
+                + "any local changes that have been made to the Process Group since it was last synchronized with the Flow Registry. This will result in the "
+                + "flow matching the Versioned Flow that exists in the Flow Registry. This can be a lengthy "
+                + "process, as it will stop any Processors and disable any Controller Services necessary to perform the action and then restart them. As a result, "
+                + "the endpoint will immediately return a VersionedFlowUpdateRequestEntity, and the process of updating the flow will occur "
+                + "asynchronously in the background. The client may then periodically poll the status of the request by issuing a GET request to "
+                + "/versions/revert-requests/{requestId}. Once the request is completed, the client is expected to issue a DELETE request to "
+                + "/versions/revert-requests/{requestId}.",
             response = VersionedFlowUpdateRequestEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
@@ -1174,34 +1245,6 @@ public class VersionsResource extends ApplicationResource {
                     throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
                 }
 
-                // If the information passed in is correct, but there have been no changes, there is nothing to do - just register the request, mark it complete, and return.
-                if (currentVersion.getModified() == Boolean.FALSE) {
-                    final String requestId = UUID.randomUUID().toString();
-                    final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Complete");
-                    requestManager.submitRequest("revert-requests", requestId, request, task -> {
-                    });
-
-                    // There is nothing to do. Generate the response and send it back to the user.
-                    final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
-                    updateRequestDto.setComplete(true);
-                    updateRequestDto.setFailureReason(null);
-                    updateRequestDto.setLastUpdated(new Date());
-                    updateRequestDto.setProcessGroupId(groupId);
-                    updateRequestDto.setRequestId(requestId);
-                    updateRequestDto.setVersionControlInformation(currentVersion);
-                    updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
-                    updateRequestDto.setPercentCompleted(100);
-                    updateRequestDto.setState(request.getState());
-
-                    final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
-                    updateRequestEntity.setProcessGroupRevision(revisionDto);
-                    updateRequestEntity.setRequest(updateRequestDto);
-
-                    request.markComplete(currentVersionEntity);
-                    return generateOkResponse(updateRequestEntity).build();
-                }
-
-
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
@@ -1331,7 +1374,25 @@ public class VersionsResource extends ApplicationResource {
             // Step 11-12. Update Process Group to the new flow and update variable registry with any Variables that were added or removed
             final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
             final Revision revision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
-            final VersionControlInformationDTO vci = requestEntity.getVersionControlInformation();
+            final VersionControlInformationDTO requestVci = requestEntity.getVersionControlInformation();
+
+            final Bucket bucket = flowSnapshot.getBucket();
+            final VersionedFlow flow = flowSnapshot.getFlow();
+
+            final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
+            final VersionControlInformationDTO vci = new VersionControlInformationDTO();
+            vci.setBucketId(metadata.getBucketIdentifier());
+            vci.setBucketName(bucket.getName());
+            vci.setCurrent(flowSnapshot.isLatest());
+            vci.setFlowDescription(flow.getDescription());
+            vci.setFlowId(flow.getIdentifier());
+            vci.setFlowName(flow.getName());
+            vci.setGroupId(groupId);
+            vci.setModified(false);
+            vci.setRegistryId(requestVci.getRegistryId());
+            vci.setRegistryName(serviceFacade.getFlowRegistryName(requestVci.getRegistryId()));
+            vci.setVersion(metadata.getVersion());
+
             serviceFacade.updateProcessGroupContents(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified, false);
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index bd603be..1a12dcf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,6 +16,33 @@
  */
 package org.apache.nifi.web.api.dto;
 
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.WebApplicationException;
+
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -188,32 +215,6 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
-import javax.ws.rs.WebApplicationException;
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
 public final class DtoFactory {
 
     @SuppressWarnings("rawtypes")
@@ -2230,8 +2231,8 @@ public final class DtoFactory {
         dto.setFlowName(versionControlInfo.getFlowName());
         dto.setFlowDescription(versionControlInfo.getFlowDescription());
         dto.setVersion(versionControlInfo.getVersion());
-        dto.setCurrent(versionControlInfo.getCurrent().orElse(true));
-        dto.setModified(versionControlInfo.getModified().orElse(false));
+        dto.setCurrent(versionControlInfo.isCurrent());
+        dto.setModified(versionControlInfo.isModified());
         return dto;
     }
 


[09/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
new file mode 100644
index 0000000..c41d13b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
@@ -0,0 +1,445 @@
+/*
+ * 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.web.util;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.cluster.coordination.ClusterCoordinator;
+import org.apache.nifi.cluster.coordination.http.replication.RequestReplicator;
+import org.apache.nifi.cluster.exception.NoClusterCoordinatorException;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.web.NiFiServiceFacade;
+import org.apache.nifi.web.Revision;
+import org.apache.nifi.web.api.ApplicationResource.ReplicationTarget;
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.DtoFactory;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO;
+import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.api.entity.ControllerServicesEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
+import org.apache.nifi.web.api.entity.ProcessorsEntity;
+import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.jersey.core.util.MultivaluedMapImpl;
+
+public class ClusterReplicationComponentLifecycle implements ComponentLifecycle {
+    private static final Logger logger = LoggerFactory.getLogger(ClusterReplicationComponentLifecycle.class);
+
+    private ClusterCoordinator clusterCoordinator;
+    private RequestReplicator requestReplicator;
+    private NiFiServiceFacade serviceFacade;
+    private DtoFactory dtoFactory;
+
+
+    @Override
+    public Set<AffectedComponentEntity> scheduleComponents(final URI exampleUri, final NiFiUser user, final String groupId, final Set<AffectedComponentEntity> components,
+            final ScheduledState desiredState, final Pause pause) throws LifecycleManagementException {
+
+        final Set<String> componentIds = components.stream()
+            .map(component -> component.getId())
+            .collect(Collectors.toSet());
+
+        final Map<String, AffectedComponentEntity> componentMap = components.stream()
+            .collect(Collectors.toMap(AffectedComponentEntity::getId, Function.identity()));
+
+        final Map<String, Revision> componentRevisionMap = getRevisions(groupId, componentIds);
+        final Map<String, RevisionDTO> componentRevisionDtoMap = componentRevisionMap.entrySet().stream().collect(
+            Collectors.toMap(Map.Entry::getKey, entry -> dtoFactory.createRevisionDTO(entry.getValue())));
+
+        final ScheduleComponentsEntity scheduleProcessorsEntity = new ScheduleComponentsEntity();
+        scheduleProcessorsEntity.setComponents(componentRevisionDtoMap);
+        scheduleProcessorsEntity.setId(groupId);
+        scheduleProcessorsEntity.setState(desiredState.name());
+
+        URI scheduleGroupUri;
+        try {
+            scheduleGroupUri = new URI(exampleUri.getScheme(), exampleUri.getUserInfo(), exampleUri.getHost(),
+                exampleUri.getPort(), "/nifi-api/flow/process-groups/" + groupId, null, exampleUri.getFragment());
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("content-type", MediaType.APPLICATION_JSON);
+
+        // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+        try {
+            final NodeResponse clusterResponse;
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(user, HttpMethod.PUT, scheduleGroupUri, scheduleProcessorsEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), user, HttpMethod.PUT, scheduleGroupUri, scheduleProcessorsEntity, headers).awaitMergedResponse();
+            }
+
+            final int scheduleComponentStatus = clusterResponse.getStatus();
+            if (scheduleComponentStatus != Status.OK.getStatusCode()) {
+                throw new LifecycleManagementException("Failed to transition components to a state of " + desiredState);
+            }
+
+            final boolean processorsTransitioned = waitForProcessorStatus(user, exampleUri, groupId, componentMap, desiredState, pause);
+
+            if (!processorsTransitioned) {
+                throw new LifecycleManagementException("Failed while waiting for components to transition to state of " + desiredState);
+            }
+        } catch (final InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            throw new LifecycleManagementException("Interrupted while attempting to transition components to state of " + desiredState);
+        }
+
+        final Set<AffectedComponentEntity> updatedEntities = components.stream()
+            .map(component -> AffectedComponentUtils.updateEntity(component, serviceFacade, dtoFactory, user))
+            .collect(Collectors.toSet());
+        return updatedEntities;
+    }
+
+
+    private ReplicationTarget getReplicationTarget() {
+        return clusterCoordinator.isActiveClusterCoordinator() ? ReplicationTarget.CLUSTER_NODES : ReplicationTarget.CLUSTER_COORDINATOR;
+    }
+
+    private RequestReplicator getRequestReplicator() {
+        return requestReplicator;
+    }
+
+    protected NodeIdentifier getClusterCoordinatorNode() {
+        final NodeIdentifier activeClusterCoordinator = clusterCoordinator.getElectedActiveCoordinatorNode();
+        if (activeClusterCoordinator != null) {
+            return activeClusterCoordinator;
+        }
+
+        throw new NoClusterCoordinatorException();
+    }
+
+    private Map<String, Revision> getRevisions(final String groupId, final Set<String> componentIds) {
+        final Set<Revision> processorRevisions = serviceFacade.getRevisionsFromGroup(groupId, group -> componentIds);
+        return processorRevisions.stream().collect(Collectors.toMap(revision -> revision.getComponentId(), Function.identity()));
+    }
+
+    /**
+     * Periodically polls the process group with the given ID, waiting for all processors whose ID's are given to have the given Scheduled State.
+     *
+     * @param user the user making the request
+     * @param groupId the ID of the Process Group to poll
+     * @param processorIds the ID of all Processors whose state should be equal to the given desired state
+     * @param desiredState the desired state for all processors with the ID's given
+     * @param pause the Pause that can be used to wait between polling
+     * @return <code>true</code> if successful, <code>false</code> if unable to wait for processors to reach the desired state
+     */
+    private boolean waitForProcessorStatus(final NiFiUser user, final URI originalUri, final String groupId, final Map<String, AffectedComponentEntity> processors,
+                final ScheduledState desiredState, final Pause pause) throws InterruptedException {
+        URI groupUri;
+        try {
+            groupUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+                originalUri.getPort(), "/nifi-api/process-groups/" + groupId + "/processors", "includeDescendantGroups=true", originalUri.getFragment());
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+
+        final Map<String, String> headers = new HashMap<>();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedMapImpl();
+
+        boolean continuePolling = true;
+        while (continuePolling) {
+
+            // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+            final NodeResponse clusterResponse;
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(user, HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), user, HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            }
+
+            if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+                return false;
+            }
+
+            final ProcessorsEntity processorsEntity = getResponseEntity(clusterResponse, ProcessorsEntity.class);
+            final Set<ProcessorEntity> processorEntities = processorsEntity.getProcessors();
+
+            if (isProcessorActionComplete(processorEntities, processors, desiredState)) {
+                logger.debug("All {} processors of interest now have the desired state of {}", processors.size(), desiredState);
+                return true;
+            }
+
+            // Not all of the processors are in the desired state. Pause for a bit and poll again.
+            continuePolling = pause.pause();
+        }
+
+        return false;
+    }
+
+    /**
+     * Extracts the response entity from the specified node response.
+     *
+     * @param nodeResponse node response
+     * @param clazz class
+     * @param <T> type of class
+     * @return the response entity
+     */
+    @SuppressWarnings("unchecked")
+    private <T> T getResponseEntity(final NodeResponse nodeResponse, final Class<T> clazz) {
+        T entity = (T) nodeResponse.getUpdatedEntity();
+        if (entity == null) {
+            entity = nodeResponse.getClientResponse().getEntity(clazz);
+        }
+        return entity;
+    }
+
+
+    private boolean isProcessorActionComplete(final Set<ProcessorEntity> processorEntities, final Map<String, AffectedComponentEntity> affectedComponents, final ScheduledState desiredState) {
+
+        final String desiredStateName = desiredState.name();
+
+        // update the affected processors
+        processorEntities.stream()
+            .filter(entity -> affectedComponents.containsKey(entity.getId()))
+            .forEach(entity -> {
+                final AffectedComponentEntity affectedComponentEntity = affectedComponents.get(entity.getId());
+                affectedComponentEntity.setRevision(entity.getRevision());
+
+                // only consider update this component if the user had permissions to it
+                if (Boolean.TRUE.equals(affectedComponentEntity.getPermissions().getCanRead())) {
+                    final AffectedComponentDTO affectedComponent = affectedComponentEntity.getComponent();
+                    affectedComponent.setState(entity.getStatus().getAggregateSnapshot().getRunStatus());
+                    affectedComponent.setActiveThreadCount(entity.getStatus().getAggregateSnapshot().getActiveThreadCount());
+
+                    if (Boolean.TRUE.equals(entity.getPermissions().getCanRead())) {
+                        affectedComponent.setValidationErrors(entity.getComponent().getValidationErrors());
+                    }
+                }
+            });
+
+        final boolean allProcessorsMatch = processorEntities.stream()
+            .filter(entity -> affectedComponents.containsKey(entity.getId()))
+            .allMatch(entity -> {
+                final ProcessorStatusDTO status = entity.getStatus();
+
+                final String runStatus = status.getAggregateSnapshot().getRunStatus();
+                final boolean stateMatches = desiredStateName.equalsIgnoreCase(runStatus);
+                if (!stateMatches) {
+                    return false;
+                }
+
+                if (desiredState == ScheduledState.STOPPED && status.getAggregateSnapshot().getActiveThreadCount() != 0) {
+                    return false;
+                }
+
+                return true;
+            });
+
+        if (!allProcessorsMatch) {
+            return false;
+        }
+
+        return true;
+    }
+
+
+
+    @Override
+    public Set<AffectedComponentEntity> activateControllerServices(final URI originalUri, final NiFiUser user, final String groupId, final Set<AffectedComponentEntity> affectedServices,
+        final ControllerServiceState desiredState, final Pause pause) throws LifecycleManagementException {
+
+        final Set<String> affectedServiceIds = affectedServices.stream()
+            .map(component -> component.getId())
+            .collect(Collectors.toSet());
+
+        final Map<String, Revision> serviceRevisionMap = getRevisions(groupId, affectedServiceIds);
+        final Map<String, RevisionDTO> serviceRevisionDtoMap = serviceRevisionMap.entrySet().stream().collect(
+            Collectors.toMap(Map.Entry::getKey, entry -> dtoFactory.createRevisionDTO(entry.getValue())));
+
+        final ActivateControllerServicesEntity activateServicesEntity = new ActivateControllerServicesEntity();
+        activateServicesEntity.setComponents(serviceRevisionDtoMap);
+        activateServicesEntity.setId(groupId);
+        activateServicesEntity.setState(desiredState.name());
+
+        URI controllerServicesUri;
+        try {
+            controllerServicesUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+                originalUri.getPort(), "/nifi-api/flow/process-groups/" + groupId + "/controller-services", null, originalUri.getFragment());
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("content-type", MediaType.APPLICATION_JSON);
+
+        // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+        try {
+            final NodeResponse clusterResponse;
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(user, HttpMethod.PUT, controllerServicesUri, activateServicesEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), user, HttpMethod.PUT, controllerServicesUri, activateServicesEntity, headers).awaitMergedResponse();
+            }
+
+            final int disableServicesStatus = clusterResponse.getStatus();
+            if (disableServicesStatus != Status.OK.getStatusCode()) {
+                throw new LifecycleManagementException("Failed to update Controller Services to a state of " + desiredState);
+            }
+
+            final boolean serviceTransitioned = waitForControllerServiceStatus(user, originalUri, groupId, affectedServiceIds, desiredState, pause);
+
+            if (!serviceTransitioned) {
+                throw new LifecycleManagementException("Failed while waiting for Controller Services to finish transitioning to a state of " + desiredState);
+            }
+        } catch (final InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            throw new LifecycleManagementException("Interrupted while transitioning Controller Services to a state of " + desiredState);
+        }
+
+        return affectedServices.stream()
+            .map(componentEntity -> serviceFacade.getControllerService(componentEntity.getId(), user))
+            .map(dtoFactory::createAffectedComponentEntity)
+            .collect(Collectors.toSet());
+    }
+
+    /**
+     * Periodically polls the process group with the given ID, waiting for all controller services whose ID's are given to have the given Controller Service State.
+     *
+     * @param user the user making the request
+     * @param groupId the ID of the Process Group to poll
+     * @param serviceIds the ID of all Controller Services whose state should be equal to the given desired state
+     * @param desiredState the desired state for all services with the ID's given
+     * @param pause the Pause that can be used to wait between polling
+     * @return <code>true</code> if successful, <code>false</code> if unable to wait for services to reach the desired state
+     */
+    private boolean waitForControllerServiceStatus(final NiFiUser user, final URI originalUri, final String groupId, final Set<String> serviceIds,
+        final ControllerServiceState desiredState, final Pause pause) throws InterruptedException {
+
+        URI groupUri;
+        try {
+            groupUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+                originalUri.getPort(), "/nifi-api/flow/process-groups/" + groupId + "/controller-services", "includeAncestorGroups=false,includeDescendantGroups=true", originalUri.getFragment());
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+
+        final Map<String, String> headers = new HashMap<>();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedMapImpl();
+
+        boolean continuePolling = true;
+        while (continuePolling) {
+
+            // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+            final NodeResponse clusterResponse;
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(user, HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), user, HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            }
+
+            if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+                return false;
+            }
+
+            final ControllerServicesEntity controllerServicesEntity = getResponseEntity(clusterResponse, ControllerServicesEntity.class);
+            final Set<ControllerServiceEntity> serviceEntities = controllerServicesEntity.getControllerServices();
+
+            final Map<String, AffectedComponentEntity> affectedServices = serviceEntities.stream()
+                .collect(Collectors.toMap(ControllerServiceEntity::getId, dtoFactory::createAffectedComponentEntity));
+
+            // update the affected controller services
+            updateAffectedControllerServices(serviceEntities, affectedServices);
+
+            final String desiredStateName = desiredState.name();
+            final boolean allServicesMatch = serviceEntities.stream()
+                .map(entity -> entity.getComponent())
+                .filter(service -> serviceIds.contains(service.getId()))
+                .map(service -> service.getState())
+                .allMatch(state -> state.equals(desiredStateName));
+
+            if (allServicesMatch) {
+                logger.debug("All {} controller services of interest now have the desired state of {}", serviceIds.size(), desiredState);
+                return true;
+            }
+
+            // Not all of the processors are in the desired state. Pause for a bit and poll again.
+            continuePolling = pause.pause();
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Updates the affected controller services in the specified updateRequest with the serviceEntities.
+     *
+     * @param serviceEntities service entities
+     * @param updateRequest update request
+     */
+    private void updateAffectedControllerServices(final Set<ControllerServiceEntity> serviceEntities, final Map<String, AffectedComponentEntity> affectedServices) {
+        // update the affected components
+        serviceEntities.stream()
+            .filter(entity -> affectedServices.containsKey(entity.getId()))
+            .forEach(entity -> {
+                final AffectedComponentEntity affectedComponentEntity = affectedServices.get(entity.getId());
+                affectedComponentEntity.setRevision(entity.getRevision());
+
+                // only consider update this component if the user had permissions to it
+                if (Boolean.TRUE.equals(affectedComponentEntity.getPermissions().getCanRead())) {
+                    final AffectedComponentDTO affectedComponent = affectedComponentEntity.getComponent();
+                    affectedComponent.setState(entity.getComponent().getState());
+
+                    if (Boolean.TRUE.equals(entity.getPermissions().getCanRead())) {
+                        affectedComponent.setValidationErrors(entity.getComponent().getValidationErrors());
+                    }
+                }
+            });
+    }
+
+    public void setClusterCoordinator(final ClusterCoordinator clusterCoordinator) {
+        this.clusterCoordinator = clusterCoordinator;
+    }
+
+    public void setRequestReplicator(final RequestReplicator requestReplicator) {
+        this.requestReplicator = requestReplicator;
+    }
+
+    public void setDtoFactory(final DtoFactory dtoFactory) {
+        this.dtoFactory = dtoFactory;
+    }
+
+    public void setServiceFacade(final NiFiServiceFacade serviceFacade) {
+        this.serviceFacade = serviceFacade;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ComponentLifecycle.java
new file mode 100644
index 0000000..c84b966
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ComponentLifecycle.java
@@ -0,0 +1,62 @@
+/*
+ * 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.web.util;
+
+import java.net.URI;
+import java.util.Set;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+
+public interface ComponentLifecycle {
+    /**
+     * Updates the scheduled state of all components that are given, to match the desired ScheduledState
+     *
+     * @param exampleUri an URI to use as a base for the REST API.
+     * @param user the user making the request
+     * @param groupId the ID of the process group
+     * @param components the components to schedule or unschedule
+     * @param desiredState the desired state of the components
+     * @param pause a pause that can be used to determine how long to wait between polling for task completion and that can also be used to cancel the operation
+     *
+     * @return the set of all AffectedComponents that are updated by the request, including the new Revisions
+     *
+     * @throws IllegalStateException if any of the components given do not have a state that can be transitioned to the given desired state
+     */
+    Set<AffectedComponentEntity> scheduleComponents(URI exampleUri, NiFiUser user, String groupId, Set<AffectedComponentEntity> components,
+        ScheduledState desiredState, Pause pause) throws LifecycleManagementException;
+
+    /**
+     * Updates the Controller Service State state of all controller services that are given, to match the desired ControllerServiceState
+     *
+     * @param exampleUri an URI to use as a base for the REST API
+     * @param user the user making the request
+     * @param groupId the ID of the process group
+     * @param services the controller services to enable or disable
+     * @param desiredState the desired state of the components
+     * @param pause a pause that can be used to determine how long to wait between polling for task completion and that can also be used to cancel the operation
+     *
+     * @return the set of all AffectedComponents that are updated by the request, including the new Revisions
+     *
+     * @throws IllegalStateException if any of the components given do not have a state that can be transitioned to the given desired state
+     */
+    Set<AffectedComponentEntity> activateControllerServices(URI exampleUri, NiFiUser user, String groupId, Set<AffectedComponentEntity> services,
+        ControllerServiceState desiredState, Pause pause) throws LifecycleManagementException;
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LifecycleManagementException.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LifecycleManagementException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LifecycleManagementException.java
new file mode 100644
index 0000000..1778c6a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LifecycleManagementException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.web.util;
+
+public class LifecycleManagementException extends Exception {
+
+    public LifecycleManagementException(String message) {
+        super(message);
+    }
+
+    public LifecycleManagementException(Throwable cause) {
+        super(cause);
+    }
+
+    public LifecycleManagementException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
new file mode 100644
index 0000000..22ffec7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
@@ -0,0 +1,312 @@
+/*
+ * 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.web.util;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.web.NiFiServiceFacade;
+import org.apache.nifi.web.Revision;
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.DtoFactory;
+import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
+import org.apache.nifi.web.revision.RevisionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LocalComponentLifecycle implements ComponentLifecycle {
+    private static final Logger logger = LoggerFactory.getLogger(LocalComponentLifecycle.class);
+
+    private NiFiServiceFacade serviceFacade;
+    private RevisionManager revisionManager;
+    private DtoFactory dtoFactory;
+
+    @Override
+    public Set<AffectedComponentEntity> scheduleComponents(final URI exampleUri, final NiFiUser user, final String groupId, final Set<AffectedComponentEntity> components,
+        final ScheduledState desiredState, final Pause pause) throws LifecycleManagementException {
+
+        final Map<String, Revision> processorRevisions = components.stream()
+            .collect(Collectors.toMap(AffectedComponentEntity::getId, entity -> revisionManager.getRevision(entity.getId())));
+
+        final Map<String, AffectedComponentEntity> affectedComponentMap = components.stream()
+            .collect(Collectors.toMap(AffectedComponentEntity::getId, Function.identity()));
+
+        if (desiredState == ScheduledState.RUNNING) {
+            startComponents(groupId, processorRevisions, affectedComponentMap, user, pause);
+        } else {
+            stopComponents(groupId, processorRevisions, affectedComponentMap, user, pause);
+        }
+
+        final Set<AffectedComponentEntity> updatedEntities = components.stream()
+            .map(component -> AffectedComponentUtils.updateEntity(component, serviceFacade, dtoFactory, user))
+            .collect(Collectors.toSet());
+        return updatedEntities;
+    }
+
+    @Override
+    public Set<AffectedComponentEntity> activateControllerServices(final URI exampleUri, final NiFiUser user, final String groupId, final Set<AffectedComponentEntity> services,
+        final ControllerServiceState desiredState, final Pause pause) throws LifecycleManagementException {
+
+        final Map<String, Revision> serviceRevisions = services.stream()
+            .collect(Collectors.toMap(AffectedComponentEntity::getId, entity -> revisionManager.getRevision(entity.getId())));
+
+        final Map<String, AffectedComponentEntity> affectedServiceMap = services.stream()
+            .collect(Collectors.toMap(AffectedComponentEntity::getId, Function.identity()));
+
+        if (desiredState == ControllerServiceState.ENABLED) {
+            enableControllerServices(groupId, serviceRevisions, affectedServiceMap, user, pause);
+        } else {
+            disableControllerServices(groupId, serviceRevisions, affectedServiceMap, user, pause);
+        }
+
+        return services.stream()
+            .map(componentEntity -> serviceFacade.getControllerService(componentEntity.getId(), user))
+            .map(dtoFactory::createAffectedComponentEntity)
+            .collect(Collectors.toSet());
+    }
+
+
+    private void startComponents(final String processGroupId, final Map<String, Revision> componentRevisions, final Map<String, AffectedComponentEntity> affectedComponents,
+        final NiFiUser user, final Pause pause) {
+
+        if (componentRevisions.isEmpty()) {
+            return;
+        }
+
+        logger.debug("Starting components with ID's {} from Process Group {}", componentRevisions.keySet(), processGroupId);
+
+        serviceFacade.verifyScheduleComponents(processGroupId, ScheduledState.RUNNING, componentRevisions.keySet());
+        serviceFacade.scheduleComponents(user, processGroupId, ScheduledState.RUNNING, componentRevisions);
+
+        // wait for all of the Processors to reach the desired state. We don't have to wait for other components because
+        // Local and Remote Ports as well as funnels start immediately.
+        waitForProcessorState(processGroupId, affectedComponents, ScheduledState.RUNNING, pause);
+    }
+
+    private void stopComponents(final String processGroupId, final Map<String, Revision> componentRevisions, final Map<String, AffectedComponentEntity> affectedComponents,
+        final NiFiUser user, final Pause pause) {
+
+        if (componentRevisions.isEmpty()) {
+            return;
+        }
+
+        logger.debug("Stopping components with ID's {} from Process Group {}", componentRevisions.keySet(), processGroupId);
+
+        serviceFacade.verifyScheduleComponents(processGroupId, ScheduledState.STOPPED, componentRevisions.keySet());
+        serviceFacade.scheduleComponents(user, processGroupId, ScheduledState.STOPPED, componentRevisions);
+
+        // wait for all of the Processors to reach the desired state. We don't have to wait for other components because
+        // Local and Remote Ports as well as funnels stop immediately.
+        waitForProcessorState(processGroupId, affectedComponents, ScheduledState.STOPPED, pause);
+    }
+
+    /**
+     * Waits for all of the given Processors to reach the given Scheduled State.
+     *
+     * @return <code>true</code> if all processors have reached the desired state, false if the given {@link Pause} indicates
+     *         to give up before all of the processors have reached the desired state
+     */
+    private boolean waitForProcessorState(final String groupId, final Map<String, AffectedComponentEntity> affectedComponents,
+        final ScheduledState desiredState, final Pause pause) {
+
+        logger.debug("Waiting for {} processors to transition their states to {}", affectedComponents.size(), desiredState);
+
+        boolean continuePolling = true;
+        while (continuePolling) {
+            final Set<ProcessorEntity> processorEntities = serviceFacade.getProcessors(groupId, true);
+
+            if (isProcessorActionComplete(processorEntities, affectedComponents, desiredState)) {
+                logger.debug("All {} processors of interest now have the desired state of {}", affectedComponents.size(), desiredState);
+                return true;
+            }
+
+            // Not all of the processors are in the desired state. Pause for a bit and poll again.
+            continuePolling = pause.pause();
+        }
+
+        return false;
+    }
+
+    private boolean isProcessorActionComplete(final Set<ProcessorEntity> processorEntities, final Map<String, AffectedComponentEntity> affectedComponents, final ScheduledState desiredState) {
+
+        final String desiredStateName = desiredState.name();
+
+        // update the affected processors
+        processorEntities.stream()
+            .filter(entity -> affectedComponents.containsKey(entity.getId()))
+            .forEach(entity -> {
+                final AffectedComponentEntity affectedComponentEntity = affectedComponents.get(entity.getId());
+                affectedComponentEntity.setRevision(entity.getRevision());
+
+                // only consider updating this component if the user had permissions to it
+                if (Boolean.TRUE.equals(affectedComponentEntity.getPermissions().getCanRead())) {
+                    final AffectedComponentDTO affectedComponent = affectedComponentEntity.getComponent();
+                    affectedComponent.setState(entity.getStatus().getAggregateSnapshot().getRunStatus());
+                    affectedComponent.setActiveThreadCount(entity.getStatus().getAggregateSnapshot().getActiveThreadCount());
+
+                    if (Boolean.TRUE.equals(entity.getPermissions().getCanRead())) {
+                        affectedComponent.setValidationErrors(entity.getComponent().getValidationErrors());
+                    }
+                }
+            });
+
+        final boolean allProcessorsMatch = processorEntities.stream()
+            .filter(entity -> affectedComponents.containsKey(entity.getId()))
+            .allMatch(entity -> {
+                final ProcessorStatusDTO status = entity.getStatus();
+
+                final String runStatus = status.getAggregateSnapshot().getRunStatus();
+                final boolean stateMatches = desiredStateName.equalsIgnoreCase(runStatus);
+                if (!stateMatches) {
+                    return false;
+                }
+
+                if (desiredState == ScheduledState.STOPPED && status.getAggregateSnapshot().getActiveThreadCount() != 0) {
+                    return false;
+                }
+
+                return true;
+            });
+
+        if (!allProcessorsMatch) {
+            return false;
+        }
+
+        return true;
+    }
+
+
+    private void enableControllerServices(final String processGroupId, final Map<String, Revision> serviceRevisions, final Map<String, AffectedComponentEntity> affectedServices,
+        final NiFiUser user, final Pause pause) {
+
+        if (serviceRevisions.isEmpty()) {
+            return;
+        }
+
+        logger.debug("Enabling Controller Services with ID's {} from Process Group {}", serviceRevisions.keySet(), processGroupId);
+
+        serviceFacade.verifyActivateControllerServices(processGroupId, ControllerServiceState.ENABLED, affectedServices.keySet());
+        serviceFacade.activateControllerServices(user, processGroupId, ControllerServiceState.ENABLED, serviceRevisions);
+        waitForControllerServiceState(processGroupId, affectedServices, ControllerServiceState.ENABLED, pause, user);
+    }
+
+    private void disableControllerServices(final String processGroupId, final Map<String, Revision> serviceRevisions, final Map<String, AffectedComponentEntity> affectedServices,
+        final NiFiUser user, final Pause pause) {
+
+        if (serviceRevisions.isEmpty()) {
+            return;
+        }
+
+        logger.debug("Disabling Controller Services with ID's {} from Process Group {}", serviceRevisions.keySet(), processGroupId);
+
+        serviceFacade.verifyActivateControllerServices(processGroupId, ControllerServiceState.DISABLED, affectedServices.keySet());
+        serviceFacade.activateControllerServices(user, processGroupId, ControllerServiceState.DISABLED, serviceRevisions);
+        waitForControllerServiceState(processGroupId, affectedServices, ControllerServiceState.DISABLED, pause, user);
+    }
+
+    /**
+     * Periodically polls the process group with the given ID, waiting for all controller services whose ID's are given to have the given Controller Service State.
+     *
+     * @param groupId the ID of the Process Group to poll
+     * @param serviceIds the ID of all Controller Services whose state should be equal to the given desired state
+     * @param desiredState the desired state for all services with the ID's given
+     * @param pause the Pause that can be used to wait between polling
+     * @param user the user that is retrieving the controller services
+     * @return <code>true</code> if successful, <code>false</code> if unable to wait for services to reach the desired state
+     */
+    private boolean waitForControllerServiceState(final String groupId, final Map<String, AffectedComponentEntity> affectedServices, final ControllerServiceState desiredState, final Pause pause,
+        final NiFiUser user) {
+
+        logger.debug("Waiting for {} Controller Services to transition their states to {}", affectedServices.size(), desiredState);
+
+        boolean continuePolling = true;
+        while (continuePolling) {
+            final Set<ControllerServiceEntity> serviceEntities = serviceFacade.getControllerServices(groupId, false, true, user);
+
+            // update the affected controller services
+            updateAffectedControllerServices(serviceEntities, affectedServices);
+
+            final String desiredStateName = desiredState.name();
+            final boolean allServicesMatch = serviceEntities.stream()
+                .map(entity -> entity.getComponent())
+                .filter(service -> affectedServices.containsKey(service.getId()))
+                .map(service -> service.getState())
+                .allMatch(state -> desiredStateName.equals(state));
+
+
+            if (allServicesMatch) {
+                logger.debug("All {} controller services of interest now have the desired state of {}", affectedServices.size(), desiredState);
+                return true;
+            }
+
+            // Not all of the processors are in the desired state. Pause for a bit and poll again.
+            continuePolling = pause.pause();
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Updates the affected controller services in the specified updateRequest with the serviceEntities.
+     *
+     * @param serviceEntities service entities
+     * @param updateRequest update request
+     */
+    private void updateAffectedControllerServices(final Set<ControllerServiceEntity> serviceEntities, final Map<String, AffectedComponentEntity> affectedServices) {
+        // update the affected components
+        serviceEntities.stream()
+            .filter(entity -> affectedServices.containsKey(entity.getId()))
+            .forEach(entity -> {
+                final AffectedComponentEntity affectedComponentEntity = affectedServices.get(entity.getId());
+                affectedComponentEntity.setRevision(entity.getRevision());
+
+                // only consider update this component if the user had permissions to it
+                if (Boolean.TRUE.equals(affectedComponentEntity.getPermissions().getCanRead())) {
+                    final AffectedComponentDTO affectedComponent = affectedComponentEntity.getComponent();
+                    affectedComponent.setState(entity.getComponent().getState());
+
+                    if (Boolean.TRUE.equals(entity.getPermissions().getCanRead())) {
+                        affectedComponent.setValidationErrors(entity.getComponent().getValidationErrors());
+                    }
+                }
+            });
+    }
+
+
+    public void setServiceFacade(final NiFiServiceFacade serviceFacade) {
+        this.serviceFacade = serviceFacade;
+    }
+
+    public void setRevisionManager(final RevisionManager revisionManager) {
+        this.revisionManager = revisionManager;
+    }
+
+    public void setDtoFactory(final DtoFactory dtoFactory) {
+        this.dtoFactory = dtoFactory;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 7fc33cf..48db565 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -61,6 +61,20 @@
         <property name="flowController" ref="flowController"/>
         <property name="accessPolicyDAO" ref="policyBasedAuthorizerDAO"/>
     </bean>
+    
+    <bean id="clusterComponentLifecycle" class="org.apache.nifi.web.util.ClusterReplicationComponentLifecycle">
+        <property name="clusterCoordinator" ref="clusterCoordinator" />
+        <property name="requestReplicator" ref="requestReplicator" />
+        <property name="serviceFacade" ref="serviceFacade" />
+        <property name="dtoFactory" ref="dtoFactory" />
+    </bean>
+
+    <bean id="localComponentLifecycle" class="org.apache.nifi.web.util.LocalComponentLifecycle">
+        <property name="serviceFacade" ref="serviceFacade" />
+        <property name="revisionManager" ref="revisionManager" />
+        <property name="dtoFactory" ref="dtoFactory" />
+    </bean>
+
 
     <!-- nifi component dao initialization -->
     <bean id="processGroupDAO" class="org.apache.nifi.web.dao.impl.StandardProcessGroupDAO">
@@ -166,6 +180,7 @@
         <property name="heartbeatMonitor" ref="heartbeatMonitor" />
         <property name="bulletinRepository" ref="bulletinRepository"/>
         <property name="leaderElectionManager" ref="leaderElectionManager" />
+        <property name="flowRegistryClient" ref="flowRegistryClient" />
     </bean>
     
     <!-- component ui extension configuration context -->
@@ -286,6 +301,17 @@
         <property name="flowController" ref="flowController" />
         <property name="dtoFactory" ref="dtoFactory" />
     </bean>
+    <bean id="versionsResource" class="org.apache.nifi.web.api.VersionsResource" scope="singleton">
+        <property name="serviceFacade" ref="serviceFacade" />
+        <property name="properties" ref="nifiProperties"/>
+        <property name="requestReplicator" ref="requestReplicator" />
+        <property name="clusterCoordinator" ref="clusterCoordinator"/>
+        <property name="flowController" ref="flowController" />
+        <property name="authorizer" ref="authorizer"/>
+        <property name="clusterComponentLifecycle" ref="clusterComponentLifecycle" />
+        <property name="localComponentLifecycle" ref="localComponentLifecycle" />
+        <property name="dtoFactory" ref="dtoFactory" />
+    </bean>
     <bean id="processorResource" class="org.apache.nifi.web.api.ProcessorResource" scope="singleton">
         <property name="serviceFacade" ref="serviceFacade"/>
         <property name="properties" ref="nifiProperties"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-optimistic-locking/src/main/java/org/apache/nifi/web/revision/NaiveRevisionManager.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-optimistic-locking/src/main/java/org/apache/nifi/web/revision/NaiveRevisionManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-optimistic-locking/src/main/java/org/apache/nifi/web/revision/NaiveRevisionManager.java
index 5911120..c9a87a2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-optimistic-locking/src/main/java/org/apache/nifi/web/revision/NaiveRevisionManager.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-optimistic-locking/src/main/java/org/apache/nifi/web/revision/NaiveRevisionManager.java
@@ -102,14 +102,13 @@ public class NaiveRevisionManager implements RevisionManager {
         final List<Revision> revisionList = new ArrayList<>(originalClaim.getRevisions());
         revisionList.sort(new RevisionComparator());
 
-        String failedId = null;
         for (final Revision revision : revisionList) {
             final Revision currentRevision = getRevision(revision.getComponentId());
             final boolean verified = revision.equals(currentRevision);
 
             if (!verified) {
                 // Throw an Exception indicating that we failed to obtain the locks
-                throw new InvalidRevisionException("Invalid Revision was given for component with ID '" + failedId + "'");
+                throw new InvalidRevisionException("Invalid Revision was given for component with ID '" + revision.getComponentId() + "'");
             }
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index bc92bcd..490862c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,6 +110,7 @@
         <hwx.registry.version>0.3.0</hwx.registry.version>
         <jackson.version>2.9.1</jackson.version>
         <atlas.version>0.8.1</atlas.version>
+        <nifi.registry.version>0.0.1-SNAPSHOT</nifi.registry.version>
     </properties>
 
     <repositories>
@@ -1630,6 +1631,16 @@
                 <version>1.5.0-SNAPSHOT</version>
             </dependency>
             <dependency>
+                <groupId>org.apache.nifi.registry</groupId>
+                <artifactId>nifi-registry-data-model</artifactId>
+                <version>${nifi.registry.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.nifi.registry</groupId>
+                <artifactId>nifi-registry-flow-diff</artifactId>
+                <version>${nifi.registry.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>com.jayway.jsonpath</groupId>
                 <artifactId>json-path</artifactId>
                 <version>2.0.0</version>


[44/50] nifi git commit: NIFI-4436: - Bumping registry version to 0.1.0. - Addressing PR comments.

Posted by bb...@apache.org.
NIFI-4436:
- Bumping registry version to 0.1.0.
- Addressing PR comments.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/20b539aa
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/20b539aa
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/20b539aa

Branch: refs/heads/master
Commit: 20b539aac3ea4d42d38df2278821c4f0021c0a2f
Parents: 0127b02
Author: Matt Gilman <ma...@gmail.com>
Authored: Tue Jan 2 10:26:52 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:56 2018 -0500

----------------------------------------------------------------------
 .../java/org/apache/nifi/web/StandardNiFiServiceFacade.java   | 7 +++++++
 .../WEB-INF/partials/canvas/save-flow-version-dialog.jsp      | 6 +++---
 pom.xml                                                       | 2 +-
 3 files changed, 11 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/20b539aa/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 1865bff..b7559ec 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -2449,6 +2449,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
         final FlowRegistry registry = registryDAO.getFlowRegistry(registryDTO.getId());
         final RevisionUpdate<FlowRegistry> revisionUpdate = revisionManager.updateRevision(revisionClaim, user, () -> {
+            final boolean duplicateName = registryDAO.getFlowRegistries().stream()
+                    .anyMatch(reg -> reg.getName().equals(registryDTO.getName()));
+
+            if (duplicateName) {
+                throw new IllegalStateException("Cannot update Flow Registry because a Flow Registry already exists with the name " + registryDTO.getName());
+            }
+
             registry.setDescription(registryDTO.getDescription());
             registry.setName(registryDTO.getName());
             registry.setURL(registryDTO.getUri());

http://git-wip-us.apache.org/repos/asf/nifi/blob/20b539aa/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
index 6017f37..35da1c0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
@@ -32,7 +32,7 @@
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Name</div>
+            <div class="setting-name">Flow Name</div>
             <div id="save-flow-version-registry-container" class="setting-field">
                 <span id="save-flow-version-process-group-id" class="hidden"></span>
                 <input type="text" id="save-flow-version-name-field" class="setting-input hidden"/>
@@ -41,14 +41,14 @@
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Description</div>
+            <div class="setting-name">Flow Description</div>
             <div class="setting-field">
                 <textarea id="save-flow-version-description-field" class="setting-input hidden"></textarea>
                 <div id="save-flow-version-description" class="hidden"></div>
             </div>
         </div>
         <div class="setting">
-            <div class="setting-name">Comments</div>
+            <div class="setting-name">Version Comments</div>
             <div class="setting-field">
                 <textarea id="save-flow-version-change-comments" class="setting-input"></textarea>
             </div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/20b539aa/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 584cac6..4d56c2a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,7 +110,7 @@
         <hwx.registry.version>0.3.0</hwx.registry.version>
         <jackson.version>2.9.1</jackson.version>
         <atlas.version>0.8.1</atlas.version>
-        <nifi.registry.version>0.0.1-SNAPSHOT</nifi.registry.version>
+        <nifi.registry.version>0.1.0</nifi.registry.version>
     </properties>
 
     <repositories>


[22/50] nifi git commit: NIFI-4436: - Adding the version number to the start version control, commit, and change version dialog. - Showing a loading item in the combo's while querying for the registries, buckets, and flows. - Adding tooltips to display v

Posted by bb...@apache.org.
NIFI-4436:
- Adding the version number to the start version control, commit, and change version dialog.
- Showing a loading item in the combo's while querying for the registries, buckets, and flows.
- Adding tooltips to display version control information on the canvas.
- Adding progress bar dialogs for changing version and reverting local changes.
- Updating canvas and breadcrumb according to the version control state.
- Updating to use registry name, bucket name, and flow name where appropriate.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/d6e54f19
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/d6e54f19
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/d6e54f19

Branch: refs/heads/master
Commit: d6e54f19ee249da45fe8d625cf68ce3d3e61380a
Parents: 6b00dff
Author: Matt Gilman <ma...@gmail.com>
Authored: Fri Nov 10 15:58:59 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:53 2018 -0500

----------------------------------------------------------------------
 .../api/dto/VersionedFlowUpdateRequestDTO.java  |  10 +-
 .../apache/nifi/web/api/VersionsResource.java   |  82 ++--
 .../org/apache/nifi/web/api/dto/DtoFactory.java |   4 +-
 .../canvas/import-flow-version-dialog.jsp       |  18 +
 .../WEB-INF/partials/canvas/navigation.jsp      |   6 +-
 .../canvas/save-flow-version-dialog.jsp         |   3 +-
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |  31 +-
 .../nifi-web-ui/src/main/webapp/css/graph.css   |   3 +-
 .../src/main/webapp/css/navigation.css          |  13 +-
 .../controllers/nf-ng-breadcrumbs-controller.js |  51 +++
 .../directives/nf-ng-breadcrumbs-directive.js   |   6 +-
 .../main/webapp/js/nf/canvas/nf-flow-version.js | 443 ++++++++++++++-----
 .../webapp/js/nf/canvas/nf-process-group.js     | 131 +++++-
 .../src/main/webapp/js/nf/nf-common.js          |  19 +
 .../views/nf-ng-breadcrumbs-directive-view.html |   9 +-
 15 files changed, 653 insertions(+), 176 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
index 013b40d..cc82af4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
@@ -32,7 +32,7 @@ public class VersionedFlowUpdateRequestDTO {
     private Date lastUpdated;
     private boolean complete = false;
     private String failureReason;
-    private int percentComplete;
+    private int percentCompleted;
     private String state;
     private VersionControlInformationDTO versionControlInformation;
 
@@ -110,11 +110,11 @@ public class VersionedFlowUpdateRequestDTO {
     }
 
     @ApiModelProperty(value = "The percentage complete for the request, between 0 and 100", readOnly = true)
-    public int getPercentComplete() {
-        return percentComplete;
+    public int getPercentCompleted() {
+        return percentCompleted;
     }
 
-    public void setPercentComplete(int percentComplete) {
-        this.percentComplete = percentComplete;
+    public void setPercentCompleted(int percentCompleted) {
+        this.percentCompleted = percentCompleted;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index b808ae6..f61b399 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,37 +17,12 @@
 
 package org.apache.nifi.web.api;
 
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.ProcessGroupAuthorizable;
@@ -94,12 +69,35 @@ import org.apache.nifi.web.util.Pause;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 @Path("/versions")
 @Api(value = "/versions", description = "Endpoint for managing version control for a flow")
@@ -776,7 +774,7 @@ public class VersionsResource extends ApplicationResource {
         updateRequestDto.setRequestId(requestId);
         updateRequestDto.setUri(generateResourceUri("versions", requestType, requestId));
         updateRequestDto.setState(asyncRequest.getState());
-        updateRequestDto.setPercentComplete(asyncRequest.getPercentComplete());
+        updateRequestDto.setPercentCompleted(asyncRequest.getPercentComplete());
 
         if (updateRequestDto.isComplete()) {
             final VersionControlInformationEntity vciEntity = serviceFacade.getVersionControlInformation(asyncRequest.getProcessGroupId());
@@ -858,7 +856,7 @@ public class VersionsResource extends ApplicationResource {
         updateRequestDto.setProcessGroupId(asyncRequest.getProcessGroupId());
         updateRequestDto.setRequestId(requestId);
         updateRequestDto.setUri(generateResourceUri("versions", requestType, requestId));
-        updateRequestDto.setPercentComplete(asyncRequest.getPercentComplete());
+        updateRequestDto.setPercentCompleted(asyncRequest.getPercentComplete());
         updateRequestDto.setState(asyncRequest.getState());
 
         if (updateRequestDto.isComplete()) {
@@ -1040,7 +1038,7 @@ public class VersionsResource extends ApplicationResource {
                 updateRequestDto.setProcessGroupId(groupId);
                 updateRequestDto.setRequestId(requestId);
                 updateRequestDto.setUri(generateResourceUri("versions", "update-requests", requestId));
-                updateRequestDto.setPercentComplete(request.getPercentComplete());
+                updateRequestDto.setPercentCompleted(request.getPercentComplete());
                 updateRequestDto.setState(request.getState());
 
                 final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
@@ -1192,7 +1190,7 @@ public class VersionsResource extends ApplicationResource {
                     updateRequestDto.setRequestId(requestId);
                     updateRequestDto.setVersionControlInformation(currentVersion);
                     updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
-                    updateRequestDto.setPercentComplete(100);
+                    updateRequestDto.setPercentCompleted(100);
                     updateRequestDto.setState(request.getState());
 
                     final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
@@ -1231,6 +1229,8 @@ public class VersionsResource extends ApplicationResource {
                 updateRequestDto.setLastUpdated(request.getLastUpdated());
                 updateRequestDto.setProcessGroupId(groupId);
                 updateRequestDto.setRequestId(requestId);
+                updateRequestDto.setState(request.getState());
+                updateRequestDto.setPercentCompleted(request.getPercentComplete());
                 updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
 
                 final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 1c1e729..bd603be 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -2230,8 +2230,8 @@ public final class DtoFactory {
         dto.setFlowName(versionControlInfo.getFlowName());
         dto.setFlowDescription(versionControlInfo.getFlowDescription());
         dto.setVersion(versionControlInfo.getVersion());
-        dto.setCurrent(versionControlInfo.getCurrent().orElse(null));
-        dto.setModified(versionControlInfo.getModified().orElse(null));
+        dto.setCurrent(versionControlInfo.getCurrent().orElse(true));
+        dto.setModified(versionControlInfo.getModified().orElse(false));
         return dto;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
index 5169c7c..5ea622a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp
@@ -39,6 +39,24 @@
                 <div id="import-flow-version-name" class="hidden"></div>
             </div>
         </div>
+        <div id="import-flow-version-container" class="setting hidden">
+            <div class="setting-name">Current Version</div>
+            <div class="setting-field">
+                <div id="import-flow-version-label"></div>
+            </div>
+        </div>
         <div id="import-flow-version-table"></div>
     </div>
+</div>
+<div id="change-version-status-dialog" layout="column" class="hidden small-dialog">
+    <div class="dialog-content">
+        <div class="setting">
+            <div class="setting-field">
+                <div id="change-version-status-message"></div>
+            </div>
+            <div class="setting-field">
+                <div id="change-version-percent-complete"></div>
+            </div>
+        </div>
+    </div>
 </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
index 86f4ba3..4700b8b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
@@ -19,7 +19,11 @@
         breadcrumbs="appCtrl.serviceProvider.breadcrumbsCtrl.getBreadcrumbs();"
         click-func="appCtrl.nf.CanvasUtils.getComponentByType('ProcessGroup').enterGroup"
         highlight-crumb-id="appCtrl.nf.CanvasUtils.getGroupId();"
-        separator-func="appCtrl.nf.Common.isDefinedAndNotNull">
+        separator-func="appCtrl.nf.Common.isDefinedAndNotNull"
+        is-tracking="appCtrl.serviceProvider.breadcrumbsCtrl.isTracking"
+        is-current="appCtrl.serviceProvider.breadcrumbsCtrl.isCurrent"
+        is-modified="appCtrl.serviceProvider.breadcrumbsCtrl.isModified"
+        get-version-control-tooltip="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlTooltip">
 </nf-breadcrumbs>
 <div id="graph-controls">
     <div id="navigation-control" class="graph-control">

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
index 5e653d2..a7f54b6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
@@ -19,9 +19,10 @@
     <div class="dialog-content">
         <div class="setting">
             <div class="setting-name">Registry</div>
-            <div class="setting-field">
+            <div id="save-flow-version-registry-container" class="setting-field">
                 <div id="save-flow-version-registry-combo" class="hidden"></div>
                 <div id="save-flow-version-registry" class="hidden"></div>
+                <div id="save-flow-version-label"></div>
             </div>
         </div>
         <div class="setting">

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index f8f5177..29a8c0e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -216,10 +216,30 @@ div.progress-label {
     Save Flow Version
  */
 
+#save-flow-version-registry-container {
+    display: flex;
+}
+
 #save-flow-version-description-field, #save-flow-version-change-comments {
     height: 85px;
 }
 
+#save-flow-version-registry-combo, #save-flow-version-registry {
+    width: 100%;
+    margin-right: 10px;
+}
+
+#save-flow-version-label {
+    flex: 0 0 30px;
+    background-color: #728E9B;
+    color: #fff;
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+    border-radius: 50%;
+    padding: 0 1px;
+}
+
 /*
     Import Flow Version
  */
@@ -227,13 +247,22 @@ div.progress-label {
 #import-flow-version-table {
     overflow: hidden;
     position: absolute;
-    top: 202px;
+    top: 190px;
     left: 0px;
     right: 0px;
     bottom: 0px;
     height: 225px;
 }
 
+#change-version-percent-complete {
+    margin-top: 10px;
+    border-radius: 0;
+}
+
+#change-version-percent-complete .ui-progressbar-value {
+    border-radius: 0;
+}
+
 /*
     Variable Registry
  */

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
index caad5bd..204b1d1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
@@ -425,8 +425,7 @@ text.process-group-name {
 text.version-control {
     font-family: FontAwesome;
     font-size: 18px;
-    fill: rgba(0, 255, 0, 0.65);
-    stroke: rgba(0, 0, 0, 0.65);
+    text-shadow: 0 0 4px rgba(255,255,255,1);
     visibility: hidden;
 }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
index fc930eb..85b499e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
@@ -278,9 +278,16 @@ rect.birdseye-brush {
     top: 8px;
 }
 
-span.breadcrumb-version-control {
-    color: #0f0;
-    text-shadow: 0px 0px 1px #000;
+span.breadcrumb-version-control-green {
+    color: #1a9964;
+}
+
+span.breadcrumb-version-control-red {
+    color: #ba554a;
+}
+
+span.breadcrumb-version-control-gray {
+    color: #666666;
 }
 
 #breadcrumbs-left-border {

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
index 5bf70b1..0b71e3c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
@@ -97,6 +97,57 @@
             },
 
             /**
+             * Whether this crumb is tracking.
+             *
+             * @param breadcrumbEntity
+             * @returns {*}
+             */
+            isTracking: function (breadcrumbEntity) {
+                return nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation);
+            },
+
+            /**
+             * Returns whether the specified version control information is current.
+             *
+             * @param breadcrumbEntity
+             * @returns {boolean}
+             */
+            isCurrent: function (breadcrumbEntity) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
+                    return breadcrumbEntity.breadcrumb.versionControlInformation.current === true;
+                }
+
+                return false;
+            },
+
+            /**
+             * Returns whether the specified version control information is current.
+             *
+             * @param versionControlInformation
+             * @returns {boolean}
+             */
+            isModified: function (breadcrumbEntity) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
+                    return breadcrumbEntity.breadcrumb.versionControlInformation.modified === true;
+                }
+
+                return false;
+            },
+
+            /**
+             * Gets the content for the version control tooltip for the specified breadcrumb.
+             *
+             * @param breadcrumbEntity
+             */
+            getVersionControlTooltip: function (breadcrumbEntity) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
+                    return nfCommon.getVersionControlTooltip(breadcrumbEntity.breadcrumb.versionControlInformation);
+                } else {
+                    return 'This Process Group is not under version control.'
+                }
+            },
+
+            /**
              * Get the breadcrumbs.
              */
             getBreadcrumbs: function () {

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
index cb8a8b2..1a549d7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
@@ -40,7 +40,11 @@
                 'breadcrumbs': '=',
                 'clickFunc': '=',
                 'highlightCrumbId': '=',
-                'separatorFunc': '='
+                'separatorFunc': '=',
+                'isTracking': '=',
+                'isCurrent': '=',
+                'isModified': '=',
+                'getVersionControlTooltip': '='
             },
             link: function (scope, element, attrs) {
                 breadcrumbsCtrl.registerMouseWheelEvent(element);

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 1652624..4eb5286 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -81,6 +81,8 @@
         $('#save-flow-version-registry-combo').combo('destroy').hide();
         $('#save-flow-version-bucket-combo').combo('destroy').hide();
 
+        $('#save-flow-version-label').text('');
+
         $('#save-flow-version-registry').text('').hide();
         $('#save-flow-version-bucket').text('').hide();
 
@@ -98,6 +100,8 @@
      * Reset the import flow version dialog.
      */
     var resetImportFlowVersionDialog = function () {
+        $('#import-flow-version-dialog').removeData('pt');
+
         $('#import-flow-version-registry-combo').combo('destroy').hide();
         $('#import-flow-version-bucket-combo').combo('destroy').hide();
         $('#import-flow-version-name-combo').combo('destroy').hide();
@@ -108,11 +112,17 @@
 
         var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
         if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) {
+            importFlowVersionGrid.setSelectedRows([]);
+            importFlowVersionGrid.resetActiveCell();
+
             var importFlowVersionData = importFlowVersionGrid.getData();
             importFlowVersionData.setItems([]);
         }
 
         $('#import-flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+
+        $('#import-flow-version-container').hide();
+        $('#import-flow-version-label').text('');
     };
 
     /**
@@ -392,9 +402,13 @@
             $('#import-flow-version-dialog').modal('refreshButtons');
         });
         importFlowVersionGrid.onDblClick.subscribe(function (e, args) {
-            changeFlowVersion().done(function () {
-                $('#import-flow-version-dialog').modal('hide');
-            });
+            if ($('#import-flow-version-label').is(':visible')) {
+                changeFlowVersion();
+            } else {
+                importFlowVersion().always(function () {
+                    $('#import-flow-version-dialog').modal('hide');
+                });
+            }
         });
 
         // wire up the dataview to the grid
@@ -417,19 +431,35 @@
      */
     var showImportFlowVersionDialog = function () {
         var pt = $('#new-process-group-dialog').data('pt');
+        $('#import-flow-version-dialog').data('pt', pt);
 
         // update the registry and bucket visibility
-        var registryCombo = $('#import-flow-version-registry-combo').show();
-        var bucketCombo = $('#import-flow-version-bucket-combo').show();
-        $('#import-flow-version-name-combo').show();
+        var registryCombo = $('#import-flow-version-registry-combo').combo('destroy').combo({
+            options: [{
+                text: 'Loading registries...',
+                value: null,
+                optionClass: 'unset',
+                disabled: true
+            }]
+        }).show();
+        var bucketCombo = $('#import-flow-version-bucket-combo').combo('destroy').combo({
+            options: [{
+                text: 'Loading buckets...',
+                value: null,
+                optionClass: 'unset',
+                disabled: true
+            }]
+        }).show();
+        $('#import-flow-version-name-combo').combo('destroy').combo({
+            options: [{
+                text: 'Loading flows...',
+                value: null,
+                optionClass: 'unset',
+                disabled: true
+            }]
+        }).show();
 
         loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, selectBucketImportVersion).done(function () {
-            // reposition the version table
-            $('#import-flow-version-table').css({
-                'top': '202px',
-                'height': '225px'
-            });
-
             // show the import dialog
             $('#import-flow-version-dialog').modal('setHeaderText', 'Import Version').modal('setButtonModel', [{
                 buttonText: 'Import',
@@ -441,8 +471,7 @@
                 disabled: disableImportOrChangeButton,
                 handler: {
                     click: function () {
-                        importFlowVersion(pt).always(function (response) {
-                            // close the dialog
+                        importFlowVersion().always(function () {
                             $('#import-flow-version-dialog').modal('hide');
                         });
                     }
@@ -599,7 +628,9 @@
     /**
      * Imports the selected flow version.
      */
-    var importFlowVersion = function (pt) {
+    var importFlowVersion = function () {
+        var pt = $('#import-flow-version-dialog').data('pt');
+
         var selectedRegistry =  $('#import-flow-version-registry-combo').combo('getSelectedOption');
         var selectedBucket =  $('#import-flow-version-bucket-combo').combo('getSelectedOption');
         var selectedFlow =  $('#import-flow-version-name-combo').combo('getSelectedOption');
@@ -673,6 +704,8 @@
      */
     var changeFlowVersion = function () {
         var changeTimer = null;
+        var changeRequest = null;
+        var cancelled = false;
 
         var processGroupId = $('#import-flow-version-process-group-id').text();
         var processGroupRevision = $('#import-flow-version-process-group-id').data('revision');
@@ -682,7 +715,34 @@
         var selectedVersionIndex = importFlowVersionGrid.getSelectedRows();
         var selectedVersion = importFlowVersionGrid.getDataItem(selectedVersionIndex[0]);
 
-        // TODO - introduce dialog to show current state with option to cancel once available
+        // update the button model of the change version status dialog
+        $('#change-version-status-dialog').modal('setButtonModel', [{
+            buttonText: 'Stop',
+            color: {
+                base: '#728E9B',
+                hover: '#004849',
+                text: '#ffffff'
+            },
+            handler: {
+                click: function () {
+                    cancelled = true;
+
+                    $('#change-version-status-dialog').modal('setButtonModel', []);
+
+                    // we are waiting for the next poll attempt
+                    if (changeTimer !== null) {
+                        // cancel it
+                        clearTimeout(changeTimer);
+
+                        // cancel the change request
+                        completeChangeRequest();
+                    }
+                }
+            }
+        }]);
+
+        // hide the import dialog immediately
+        $('#import-flow-version-dialog').modal('hide');
 
         var submitChangeRequest = function () {
             var changeVersionRequest = {
@@ -706,69 +766,116 @@
                 url: '../nifi-api/versions/update-requests/process-groups/' + encodeURIComponent(processGroupId),
                 dataType: 'json',
                 contentType: 'application/json'
-            }).done(function (response) {
-                console.log(response);
+            }).done(function () {
+                // initialize the progress bar value
+                updateProgress(0);
+
+                // show the progress dialog
+                $('#change-version-status-dialog').modal('show');
             }).fail(nfErrorHandler.handleAjaxError);
         };
 
-        var pollChangeRequest = function (changeRequest) {
-            getChangeRequest(changeRequest).done(function (response) {
-                processChangeResponse(response);
-            })
+        var pollChangeRequest = function () {
+            getChangeRequest().done(processChangeResponse);
         };
 
-        var getChangeRequest = function (changeRequest) {
+        var getChangeRequest = function () {
             return $.ajax({
                 type: 'GET',
                 url: changeRequest.uri,
                 dataType: 'json'
-            }).fail(nfErrorHandler.handleAjaxError);
+            }).fail(completeChangeRequest).fail(nfErrorHandler.handleAjaxError);
         };
 
-        var deleteChangeRequest = function (changeRequest) {
-            var deleteXhr = $.ajax({
-                type: 'DELETE',
-                url: changeRequest.uri,
-                dataType: 'json'
-            }).fail(nfErrorHandler.handleAjaxError);
+        var completeChangeRequest = function () {
+            if (cancelled === true) {
+                // update the message to indicate successful completion
+                $('#change-version-status-message').text('The change version request has been cancelled.');
 
-            updateProcessGroup(processGroupId);
+                // update the button model
+                $('#change-version-status-dialog').modal('setButtonModel', [{
+                    buttonText: 'Close',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }]);
+            }
 
-            nfDialog.showOkDialog({
-                headerText: 'Change Version',
-                dialogContent: 'This Process Group version has changed.'
-            });
+            if (nfCommon.isDefinedAndNotNull(changeRequest)) {
+                $.ajax({
+                    type: 'DELETE',
+                    url: changeRequest.uri,
+                    dataType: 'json'
+                }).done(function (response) {
+                    changeRequest = response.request;
 
-            return deleteXhr;
-        };
+                    // update the component that was changing
+                    updateProcessGroup(processGroupId);
 
-        var processChangeResponse = function (response) {
-            var changeRequest = response.request;
+                    if (nfCommon.isDefinedAndNotNull(changeRequest.failureReason)) {
+                        // hide the progress dialog
+                        $('#change-version-status-dialog').modal('hide');
 
-            if (nfCommon.isDefinedAndNotNull(changeRequest.failureReason)) {
-                nfDialog.showOkDialog({
-                    headerText: 'Change Version',
-                    dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
+                        nfDialog.showOkDialog({
+                            headerText: 'Change Version',
+                            dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
+                        });
+                    } else {
+                        // update the percent complete
+                        updateProgress(changeRequest.percentCompleted);
+
+                        // update the message to indicate successful completion
+                        $('#change-version-status-message').text('This Process Group version has changed.');
+
+                        // update the button model
+                        $('#change-version-status-dialog').modal('setButtonModel', [{
+                            buttonText: 'Close',
+                            color: {
+                                base: '#728E9B',
+                                hover: '#004849',
+                                text: '#ffffff'
+                            },
+                            handler: {
+                                click: function () {
+                                    $(this).modal('hide');
+                                }
+                            }
+                        }]);
+                    }
                 });
             }
+        };
 
-            if (changeRequest.complete === true) {
-                deleteChangeRequest(changeRequest);
+        var processChangeResponse = function (response) {
+            changeRequest = response.request;
+
+            if (changeRequest.complete === true || cancelled === true) {
+                completeChangeRequest();
             } else {
+                // update the percent complete
+                updateProgress(changeRequest.percentCompleted);
+
+                // update the status of the listing request
+                $('#change-version-status-message').text(changeRequest.state);
+
                 changeTimer = setTimeout(function () {
                     // clear the timer since we've been invoked
                     changeTimer = null;
 
                     // poll revert request
-                    pollChangeRequest(changeRequest);
+                    pollChangeRequest();
                 }, 2000);
             }
         };
 
-        submitChangeRequest().done(function (response) {
-            processChangeResponse(response);
-        });
-
+        submitChangeRequest().done(processChangeResponse);
     };
 
     /**
@@ -826,7 +933,7 @@
      */
     var updateProcessGroup = function (processGroupId) {
         if (nfCanvasUtils.getGroupId() === processGroupId) {
-            // if reverting current PG... reload/refresh this group/canvas
+            // if reverting/changing current PG... reload/refresh this group/canvas
 
             // TODO consider implementing this differently
             $.ajax({
@@ -842,6 +949,23 @@
         }
     };
 
+    /**
+     * Updates the progress bar to the specified percent complete.
+     *
+     * @param percentComplete
+     */
+    var updateProgress = function (percentComplete) {
+        // remove existing labels
+        var progressBar = $('#change-version-percent-complete');
+        progressBar.find('div.progress-label').remove();
+        progressBar.find('md-progress-linear').remove();
+
+        // update the progress
+        var label = $('<div class="progress-label"></div>').text(percentComplete + '%');
+        (nfNgBridge.injector.get('$compile')($('<md-progress-linear ng-cloak ng-value="' + percentComplete + '" class="md-hue-2" md-mode="determinate" aria-label="Searching Queue"></md-progress-linear>'))(nfNgBridge.rootScope)).appendTo(progressBar);
+        progressBar.append(label);
+    };
+
     return {
         init: function (timeOffset) {
             serverTimeOffset = timeOffset;
@@ -913,6 +1037,18 @@
                 }
             });
 
+            // configure the drop request status dialog
+            $('#change-version-status-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Change Flow Version',
+                handler: {
+                    close: function () {
+                        // clear the current button model
+                        $('#change-version-status-dialog').modal('setButtonModel', []);
+                    }
+                }
+            });
+
             // handle the click for the process group import
             $('#import-process-group-link').on('click', function() {
                 showImportFlowVersionDialog();
@@ -939,26 +1075,50 @@
                         var versionControlInformation = groupVersionControlInformation.versionControlInformation;
 
                         // update the registry and bucket visibility
-                        $('#save-flow-version-registry').text(versionControlInformation.registryId).show();
-                        $('#save-flow-version-bucket').text(versionControlInformation.bucketId).show();
+                        $('#save-flow-version-registry').text(versionControlInformation.registryName).show();
+                        $('#save-flow-version-bucket').text(versionControlInformation.bucketName).show();
+                        $('#save-flow-version-label').text(versionControlInformation.version + 1);
 
                         $('#save-flow-version-name').text(versionControlInformation.flowName).show();
-                        $('#save-flow-version-description').text('Flow description goes here').show();
+                        $('#save-flow-version-description').text(versionControlInformation.flowDescription).show();
 
                         // record the versionControlInformation
                         $('#save-flow-version-process-group-id').data('versionControlInformation', versionControlInformation);
 
+                        // reposition the version label
+                        $('#save-flow-version-label').css('margin-top', '-15px');
+
                         focusName = false;
                         deferred.resolve();
                     } else {
                         // update the registry and bucket visibility
-                        $('#save-flow-version-registry-combo').show();
-                        $('#save-flow-version-bucket-combo').show();
+                        var registryCombo = $('#save-flow-version-registry-combo').combo('destroy').combo({
+                            options: [{
+                                text: 'Loading registries...',
+                                value: null,
+                                optionClass: 'unset',
+                                disabled: true
+                            }]
+                        }).show();
+                        var bucketCombo = $('#save-flow-version-bucket-combo').combo('destroy').combo({
+                            options: [{
+                                text: 'Loading buckets...',
+                                value: null,
+                                optionClass: 'unset',
+                                disabled: true
+                            }]
+                        }).show();
+
+                        // set the initial version
+                        $('#save-flow-version-label').text(1);
 
                         $('#save-flow-version-name-field').show();
                         $('#save-flow-version-description-field').show();
 
-                        loadRegistries($('#save-flow-version-dialog'), $('#save-flow-version-registry-combo'), $('#save-flow-version-bucket-combo'), selectBucketSaveFlowVersion).done(function () {
+                        // reposition the version label
+                        $('#save-flow-version-label').css('margin-top', '0');
+
+                        loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, selectBucketSaveFlowVersion).done(function () {
                             deferred.resolve();
                         }).fail(function () {
                             deferred.reject();
@@ -996,8 +1156,37 @@
                     getVersionControlInformation(processGroupId).done(function (response) {
                         if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
                             var revertTimer = null;
+                            var revertRequest = null;
+                            var cancelled = false;
+
+                            // update the button model of the revert status dialog
+                            $('#change-version-status-dialog').modal('setButtonModel', [{
+                                buttonText: 'Stop',
+                                color: {
+                                    base: '#728E9B',
+                                    hover: '#004849',
+                                    text: '#ffffff'
+                                },
+                                handler: {
+                                    click: function () {
+                                        cancelled = true;
+
+                                        $('#change-version-status-dialog').modal('setButtonModel', []);
+
+                                        // we are waiting for the next poll attempt
+                                        if (revertTimer !== null) {
+                                            // cancel it
+                                            clearTimeout(revertTimer);
+
+                                            // cancel the revert request
+                                            completeRevertRequest();
+                                        }
+                                    }
+                                }
+                            }]);
 
-                            // TODO - introduce dialog to show current state once available
+                            // hide the import dialog immediately
+                            $('#import-flow-version-dialog').modal('hide');
 
                             var submitRevertRequest = function () {
                                 var revertFlowVersionRequest = {
@@ -1015,66 +1204,116 @@
                                     url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
                                     dataType: 'json',
                                     contentType: 'application/json'
+                                }).done(function () {
+                                    // initialize the progress bar value
+                                    updateProgress(0);
+
+                                    // show the progress dialog
+                                    $('#change-version-status-dialog').modal('show');
                                 }).fail(nfErrorHandler.handleAjaxError);
                             };
 
-                            var pollRevertRequest = function (revertRequest) {
-                                getRevertRequest(revertRequest).done(function (response) {
-                                    processRevertResponse(response);
-                                })
+                            var pollRevertRequest = function () {
+                                getRevertRequest().done(processRevertResponse);
                             };
 
-                            var getRevertRequest = function (revertRequest) {
+                            var getRevertRequest = function () {
                                 return $.ajax({
                                     type: 'GET',
                                     url: revertRequest.uri,
                                     dataType: 'json'
-                                }).fail(nfErrorHandler.handleAjaxError);
+                                }).fail(completeRevertRequest).fail(nfErrorHandler.handleAjaxError);
                             };
 
-                            var deleteRevertRequest = function (revertRequest) {
-                                var deleteXhr = $.ajax({
-                                    type: 'DELETE',
-                                    url: revertRequest.uri,
-                                    dataType: 'json'
-                                }).fail(nfErrorHandler.handleAjaxError);
-
-                                updateProcessGroup(processGroupId);
-
-                                nfDialog.showOkDialog({
-                                    headerText: 'Revert Changes',
-                                    dialogContent: 'This Process Group has been reverted.'
-                                });
+                            var completeRevertRequest = function () {
+                                if (cancelled === true) {
+                                    // update the message to indicate successful completion
+                                    $('#change-version-status-message').text('The revert request has been cancelled.');
+
+                                    // update the button model
+                                    $('#change-version-status-dialog').modal('setButtonModel', [{
+                                        buttonText: 'Close',
+                                        color: {
+                                            base: '#728E9B',
+                                            hover: '#004849',
+                                            text: '#ffffff'
+                                        },
+                                        handler: {
+                                            click: function () {
+                                                $(this).modal('hide');
+                                            }
+                                        }
+                                    }]);
+                                }
 
-                                return deleteXhr;
+                                if (nfCommon.isDefinedAndNotNull(revertRequest)) {
+                                    $.ajax({
+                                        type: 'DELETE',
+                                        url: revertRequest.uri,
+                                        dataType: 'json'
+                                    }).done(function (response) {
+                                        revertRequest = response.request;
+
+                                        // update the component that was changing
+                                        updateProcessGroup(processGroupId);
+
+                                        if (nfCommon.isDefinedAndNotNull(revertRequest.failureReason)) {
+                                            // hide the progress dialog
+                                            $('#change-version-status-dialog').modal('hide');
+
+                                            nfDialog.showOkDialog({
+                                                headerText: 'Revert Local Changes',
+                                                dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
+                                            });
+                                        } else {
+                                            // update the percent complete
+                                            updateProgress(revertRequest.percentCompleted);
+
+                                            // update the message to indicate successful completion
+                                            $('#change-version-status-message').text('This Process Group version has changed.');
+
+                                            // update the button model
+                                            $('#change-version-status-dialog').modal('setButtonModel', [{
+                                                buttonText: 'Close',
+                                                color: {
+                                                    base: '#728E9B',
+                                                    hover: '#004849',
+                                                    text: '#ffffff'
+                                                },
+                                                handler: {
+                                                    click: function () {
+                                                        $(this).modal('hide');
+                                                    }
+                                                }
+                                            }]);
+                                        }
+                                    });
+                                }
                             };
 
                             var processRevertResponse = function (response) {
-                                var revertRequest = response.request;
-
-                                if (nfCommon.isDefinedAndNotNull(revertRequest.failureReason)) {
-                                    nfDialog.showOkDialog({
-                                        headerText: 'Revert Changes',
-                                        dialogContent: nfCommon.escapeHtml(revertRequest.failureReason)
-                                    });
-                                }
+                                revertRequest = response.request;
 
-                                if (revertRequest.complete === true) {
-                                    deleteRevertRequest(revertRequest);
+                                if (revertRequest.complete === true || cancelled === true) {
+                                    completeRevertRequest();
                                 } else {
+                                    // update the percent complete
+                                    updateProgress(revertRequest.percentCompleted);
+
+                                    // update the status of the revert request
+                                    $('#change-version-status-message').text(revertRequest.state);
+
                                     revertTimer = setTimeout(function () {
                                         // clear the timer since we've been invoked
                                         revertTimer = null;
 
                                         // poll revert request
-                                        pollRevertRequest(revertRequest);
+                                        pollRevertRequest();
                                     }, 2000);
                                 }
                             };
 
-                            submitRevertRequest().done(function (response) {
-                                processRevertResponse(response);
-                            });
+                            submitRevertRequest().done(processRevertResponse);
                         } else {
                             nfDialog.showOkDialog({
                                 headerText: 'Revert Changes',
@@ -1098,9 +1337,13 @@
                         var versionControlInformation = groupVersionControlInformation.versionControlInformation;
 
                         // update the registry and bucket visibility
-                        $('#import-flow-version-registry').text(versionControlInformation.registryId).show();
-                        $('#import-flow-version-bucket').text(versionControlInformation.bucketId).show();
-                        $('#import-flow-version-name').text(versionControlInformation.flowId).show();
+                        $('#import-flow-version-registry').text(versionControlInformation.registryName).show();
+                        $('#import-flow-version-bucket').text(versionControlInformation.bucketName).show();
+                        $('#import-flow-version-name').text(versionControlInformation.flowName).show();
+
+                        // show the current version information
+                        $('#import-flow-version-container').show();
+                        $('#import-flow-version-label').text(versionControlInformation.version);
 
                         // record the versionControlInformation
                         $('#import-flow-version-process-group-id').data('versionControlInformation', versionControlInformation).data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId);
@@ -1126,12 +1369,6 @@
                     }
                 }).fail(nfErrorHandler.handleAjaxError);
             }).done(function () {
-                // reposition the version table
-                $('#import-flow-version-table').css({
-                    'top': '150px',
-                    'height': '277px'
-                });
-
                 // show the dialog
                 $('#import-flow-version-dialog').modal('setHeaderText', 'Change Version').modal('setButtonModel', [{
                     buttonText: 'Change',
@@ -1143,9 +1380,7 @@
                     disabled: disableImportOrChangeButton,
                     handler: {
                         click: function () {
-                            changeFlowVersion().done(function () {
-                                $('#import-flow-version-dialog').modal('hide');
-                            });
+                            changeFlowVersion();
                         }
                     }
                 }, {

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 17bb069..7a61363 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -99,6 +99,15 @@
     };
 
     /**
+     * Determines whether the specified process group is under version control.
+     *
+     * @param d
+     */
+    var isUnderVersionControl = function (d) {
+        return nfCommon.isDefinedAndNotNull(d.component.versionControlInformation);
+    };
+
+    /**
      * Selects the process group elements against the current process group map.
      */
     var select = function () {
@@ -183,11 +192,10 @@
         // process group name
         processGroup.append('text')
             .attr({
-                'x': 3,
-                'y': 17,
+                'x': 10,
+                'y': 21,
                 'class': 'version-control'
-            })
-            .text('\uf00c');
+            });
 
         // always support selecting and navigation
         processGroup.on('dblclick', function (d) {
@@ -848,6 +856,77 @@
                     });
 
                 if (processGroupData.permissions.canRead) {
+                    // update version control information
+                    var versionControl = processGroup.select('text.version-control')
+                        .style({
+                            'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
+                            'fill': function () {
+                                if (isUnderVersionControl(processGroupData)) {
+                                    var modified = processGroupData.component.versionControlInformation.modified;
+                                    var current = processGroupData.component.versionControlInformation.current;
+                                    if (modified === true && current === false) {
+                                        return '#BA554A';
+                                    } else if (current === false) {
+                                        return '#BA554A';
+                                    } else if (modified === true) {
+                                        return '#666666';
+                                    } else {
+                                        return '#1A9964';
+                                    }
+                                } else {
+                                    return '#000';
+                                }
+                            }
+                        })
+                        .text(function () {
+                            if (isUnderVersionControl(processGroupData)) {
+                                var modified = processGroupData.component.versionControlInformation.modified;
+                                var current = processGroupData.component.versionControlInformation.current;
+                                if (modified === true && current === false) {
+                                    return '\uf06a';
+                                } else if (current === false) {
+                                    return '\uf0aa';
+                                } else if (modified === true) {
+                                    return '\uf069';
+                                } else {
+                                    return '\uf00c';
+                                }
+                            } else {
+                                return '';
+                            }
+                        }).each(function () {
+                            // get the tip
+                            var tip = d3.select('#version-control-tip-' + processGroupData.id);
+
+                            // if there are validation errors generate a tooltip
+                            if (isUnderVersionControl(processGroupData)) {
+                                // create the tip if necessary
+                                if (tip.empty()) {
+                                    tip = d3.select('#process-group-tooltips').append('div')
+                                        .attr('id', function () {
+                                            return 'version-control-tip-' + processGroupData.id;
+                                        })
+                                        .attr('class', 'tooltip nifi-tooltip');
+                                }
+
+                                // update the tip
+                                tip.html(function () {
+                                    var vci = processGroupData.component.versionControlInformation;
+                                    var versionControlTip = $('<div></div>').text('Tracking to "' + vci.flowName + '" version ' + vci.version + ' in "' + vci.registryName + ' - ' + vci.bucketName + '"');
+                                    var versionControlStateTip = $('<div></div>').text(nfCommon.getVersionControlTooltip(vci));
+                                    return $('<div></div>').append(versionControlTip).append('<br/>').append(versionControlStateTip).html();
+                                });
+
+                                // add the tooltip
+                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                            } else {
+                                // remove the tip if necessary
+                                if (!tip.empty()) {
+                                    tip.remove();
+                                }
+                            }
+                        });
+
                     // update the process group comments
                     details.select('text.process-group-comments')
                         .each(function (d) {
@@ -866,6 +945,25 @@
 
                     // update the process group name
                     processGroup.select('text.process-group-name')
+                        .attr({
+                            'x': function () {
+                                if (isUnderVersionControl(processGroupData)) {
+                                    var versionControlX = parseInt(versionControl.attr('x'), 10);
+                                    return versionControlX + Math.round(versionControl.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                                } else {
+                                    return 10;
+                                }
+                            },
+                            'width': function () {
+                                if (isUnderVersionControl(processGroupData)) {
+                                    var versionControlX = parseInt(versionControl.attr('x'), 10);
+                                    var processGroupNameX = parseInt(d3.select(this).attr('x'), 10);
+                                    return 316 - (processGroupNameX - versionControlX);
+                                } else {
+                                    return 316;
+                                }
+                            }
+                        })
                         .each(function (d) {
                             var processGroupName = d3.select(this);
 
@@ -874,21 +972,25 @@
 
                             // apply ellipsis to the process group name as necessary
                             nfCanvasUtils.ellipsis(processGroupName, d.component.name);
-                        }).append('title').text(function (d) {
-                        return d.component.name;
-                    });
-
-                    // update version control information
-                    processGroup.select('text.version-control').style('visibility', nfCommon.isDefinedAndNotNull(processGroupData.component.versionControlInformation) ? 'visible' : 'hidden');
+                        })
+                        .append('title')
+                        .text(function (d) {
+                            return d.component.name;
+                        });
                 } else {
+                    // update version control information
+                    processGroup.select('text.version-control').style('visibility', false).text('');
+
                     // clear the process group comments
                     details.select('text.process-group-comments').text(null);
 
                     // clear the process group name
-                    processGroup.select('text.process-group-name').text(null);
-
-                    // update version control information
-                    processGroup.select('text.version-control').style('visibility', false);
+                    processGroup.select('text.process-group-name')
+                        .attr({
+                            'x': 10,
+                            'width': 316
+                        })
+                        .text(null);
                 }
 
                 // populate the stats
@@ -1033,6 +1135,7 @@
         removed.each(function (d) {
             // remove any associated tooltips
             $('#bulletin-tip-' + d.id).remove();
+            $('#version-control-tip-' + d.id).remove();
         });
     };
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index 5619bc3..b3671a7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -420,6 +420,25 @@
         },
 
         /**
+         * Gets the version control tooltip.
+         *
+         * @param versionControlInformation
+         */
+        getVersionControlTooltip: function (versionControlInformation) {
+            var modified = versionControlInformation.modified;
+            var current = versionControlInformation.current;
+            if (modified === true && current === false) {
+                return 'Local changes have been made and a newer version of this flow is available';
+            } else if (current === false) {
+                return 'A newer version of this flow is available';
+            } else if (modified === true) {
+                return 'Local changes have been made';
+            } else {
+                return 'Flow version is current';
+            }
+        },
+
+        /**
          * Formats the class name of this component.
          *
          * @param dataContext component datum

http://git-wip-us.apache.org/repos/asf/nifi/blob/d6e54f19/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
index a8b8579..3e5d87b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
@@ -22,7 +22,14 @@ limitations under the License.
                 <span ng-if="separatorFunc(crumb.parentBreadcrumb)" style="margin: 0 12px;">
                     &raquo;
                 </span>
-                <span ng-if="separatorFunc(crumb.breadcrumb.versionControlInformation)" class="breadcrumb-version-control fa fa-check" style="margin: 0 6px;"></span>
+                <span ng-if="isTracking(crumb) && isCurrent(crumb) && !isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
+                      class="breadcrumb-version-control-green fa fa-check" style="margin: 0 6px;"></span>
+                <span ng-if="isTracking(crumb) && isCurrent(crumb) && isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
+                      class="breadcrumb-version-control-gray fa fa-asterisk" style="margin: 0 6px;"></span>
+                <span ng-if="isTracking(crumb) && !isCurrent(crumb) && !isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
+                      class="breadcrumb-version-control-red fa fa-arrow-circle-up" style="margin: 0 6px;"></span>
+                <span ng-if="isTracking(crumb) && !isCurrent(crumb) && isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
+                      class="breadcrumb-version-control-red fa fa-exclamation-circle" style="margin: 0 6px;"></span>
                 <span class="link"
                       ng-class="(highlightCrumbId === crumb.id) ? 'link-bold' : ''"
                       ng-click="clickFunc(crumb.id)">


[39/50] nifi git commit: NIFI-4436: Bug fixes

Posted by bb...@apache.org.
NIFI-4436: Bug fixes

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/118667a6
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/118667a6
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/118667a6

Branch: refs/heads/master
Commit: 118667a60182b5a73ac8cdd3c843f8827d9b4389
Parents: 20b539a
Author: Mark Payne <ma...@hotmail.com>
Authored: Tue Jan 2 13:09:09 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:56 2018 -0500

----------------------------------------------------------------------
 .../nifi/groups/StandardProcessGroup.java       | 34 ++++++++++++++++++--
 .../web/dao/impl/StandardProcessGroupDAO.java   |  1 +
 2 files changed, 33 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/118667a6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index bc5ef29..83468cf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -3284,7 +3284,23 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
 
             final Set<String> knownVariables = getKnownVariableNames();
-            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings, updateDescendantVersionedFlows, knownVariables);
+
+            final StandardVersionControlInformation originalVci = this.versionControlInfo.get();
+            try {
+                updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings, updateDescendantVersionedFlows, knownVariables);
+            } catch (final Throwable t) {
+                // The proposed snapshot may not have any Versioned Flow Coordinates. As a result, the call to #updateProcessGroup may
+                // set this PG's Version Control Info to null. During the normal flow of control,
+                // the Version Control Information is set appropriately at the end. However, if an Exception is thrown, we need to ensure
+                // that we don't leave the Version Control Info as null. It's also important to note here that the Atomic Reference is used
+                // as a means of retrieving the value without obtaining a read lock, but the value is never updated outside of a write lock.
+                // As a result, it is safe to use the get() and then the set() methods of the AtomicReference without introducing the 'check-then-modify' problem.
+                if (this.versionControlInfo.get() == null) {
+                    this.versionControlInfo.set(originalVci);
+                }
+
+                throw t;
+            }
         } catch (final ProcessorInstantiationException pie) {
             throw new IllegalStateException("Failed to update flow", pie);
         } finally {
@@ -3613,7 +3629,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             if (connection == null) {
                 final Connection added = addConnection(group, proposedConnection, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
-            } else if (!connection.getSource().isRunning() && !connection.getDestination().isRunning()) {
+            } else if (isUpdateable(connection)) {
                 // If the connection needs to be updated, then the source and destination will already have
                 // been stopped (else, the validation above would fail). So if the source or the destination is running,
                 // then we know that we don't need to update the connection.
@@ -3690,6 +3706,20 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    private boolean isUpdateable(final Connection connection) {
+        final Connectable source = connection.getSource();
+        if (source.getConnectableType() != ConnectableType.FUNNEL && source.isRunning()) {
+            return false;
+        }
+
+        final Connectable destination = connection.getDestination();
+        if (destination.getConnectableType() != ConnectableType.FUNNEL && destination.isRunning()) {
+            return false;
+        }
+
+        return true;
+    }
+
     private String generateUuid(final String propposedId, final String destinationGroupId, final String seed) {
         long msb = UUID.nameUUIDFromBytes((propposedId + destinationGroupId).getBytes(StandardCharsets.UTF_8)).getMostSignificantBits();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/118667a6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index e7e85af..5bbb56f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -275,6 +275,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     public ProcessGroup disconnectVersionControl(final String groupId) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
         group.disconnectVersionControl(true);
+        group.onComponentModified();
         return group;
     }
 


[08/50] nifi git commit: NIFI-4436: - Adding support to save a version of a flow based on a selected Process Group. - Adding support for revert changes back to the most recent version. - Adding support to disconnect from version control. - Moving the ver

Posted by bb...@apache.org.
NIFI-4436:
- Adding support to save a version of a flow based on a selected Process Group.
- Adding support for revert changes back to the most recent version.
- Adding support to disconnect from version control.
- Moving the version control information out of the entity objects and into the dto's.
- Fixing checkstyle issues.
NIFI-4502:
- Updating the UI to allow for the user to register registry clients.
- Updating the version control menu item names.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/7a0a900a
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/7a0a900a
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/7a0a900a

Branch: refs/heads/master
Commit: 7a0a900a0ff8b9329296b9a19b9239b63fc2d76d
Parents: 6a58d78
Author: Matt Gilman <ma...@gmail.com>
Authored: Thu Oct 12 12:29:05 2017 -0400
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:52 2018 -0500

----------------------------------------------------------------------
 .../org/apache/nifi/web/api/dto/BucketDTO.java  |  69 +++
 .../web/api/dto/ControllerConfigurationDTO.java |   1 +
 .../nifi/web/api/dto/FlowConfigurationDTO.java  |  15 +
 .../apache/nifi/web/api/dto/RegistryDTO.java    |  69 +++
 .../nifi/web/api/dto/VersionedFlowDTO.java      |  10 +
 .../web/api/dto/flow/FlowBreadcrumbDTO.java     |  16 +
 .../nifi/web/api/entity/BucketEntity.java       |  39 ++
 .../nifi/web/api/entity/BucketsEntity.java      |  41 ++
 .../nifi/web/api/entity/RegistriesEntity.java   |  41 ++
 .../nifi/web/api/entity/RegistryEntity.java     |  39 ++
 .../entity/VersionControlInformationEntity.java |   6 +-
 .../org/apache/nifi/groups/ProcessGroup.java    |   7 +-
 .../apache/nifi/registry/flow/FlowRegistry.java |  17 +
 .../nifi-framework/nifi-framework-core/pom.xml  |   5 +
 .../nifi/groups/StandardProcessGroup.java       |  58 ++-
 .../flow/FileBasedFlowRegistryClient.java       |  49 +-
 .../service/mock/MockProcessGroup.java          |  25 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  75 ++-
 .../nifi/web/NiFiWebApiResourceConfig.java      |   1 +
 .../nifi/web/StandardNiFiServiceFacade.java     | 142 ++++--
 .../apache/nifi/web/api/ControllerResource.java | 298 ++++++++++++
 .../org/apache/nifi/web/api/FlowResource.java   | 113 +++++
 .../apache/nifi/web/api/VersionsResource.java   | 156 +++---
 .../api/concurrent/AsynchronousWebRequest.java  |   6 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  74 +--
 .../apache/nifi/web/api/dto/EntityFactory.java  |   1 +
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |  18 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |  20 +-
 .../ClusterReplicationComponentLifecycle.java   |  37 +-
 .../nifi/web/util/LocalComponentLifecycle.java  |  16 +-
 .../src/main/resources/nifi-web-api-context.xml |   3 +
 .../nifi-framework/nifi-web/nifi-web-ui/pom.xml |   1 +
 .../main/resources/filters/canvas.properties    |   1 +
 .../src/main/webapp/WEB-INF/pages/canvas.jsp    |   2 +
 .../WEB-INF/partials/canvas/canvas-header.jsp   |  16 +-
 .../canvas/registry-configuration-dialog.jsp    |  40 ++
 .../canvas/save-flow-version-dialog.jsp         |  54 +++
 .../partials/canvas/settings-content.jsp        |   3 +
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |  16 +
 .../nifi-web-ui/src/main/webapp/css/graph.css   |   8 +
 .../src/main/webapp/css/navigation.css          |   5 +
 .../controllers/nf-ng-breadcrumbs-controller.js |  15 +
 .../nf-ng-canvas-graph-controls-controller.js   |   2 +-
 .../src/main/webapp/js/nf/canvas/nf-actions.js  |  54 ++-
 .../webapp/js/nf/canvas/nf-canvas-bootstrap.js  |  11 +-
 .../main/webapp/js/nf/canvas/nf-canvas-utils.js |  11 +-
 .../src/main/webapp/js/nf/canvas/nf-canvas.js   |  18 +
 .../webapp/js/nf/canvas/nf-component-state.js   |   2 +-
 .../webapp/js/nf/canvas/nf-component-version.js |   2 +-
 .../main/webapp/js/nf/canvas/nf-context-menu.js |  97 +++-
 .../main/webapp/js/nf/canvas/nf-flow-version.js | 476 +++++++++++++++++++
 .../nf/canvas/nf-process-group-configuration.js |   2 +-
 .../webapp/js/nf/canvas/nf-process-group.js     |  15 +
 .../src/main/webapp/js/nf/canvas/nf-settings.js | 392 ++++++++++++++-
 .../views/nf-ng-breadcrumbs-directive-view.html |   1 +
 55 files changed, 2465 insertions(+), 246 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BucketDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BucketDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BucketDTO.java
new file mode 100644
index 0000000..3c004c7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BucketDTO.java
@@ -0,0 +1,69 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * Details about a bucket in a registry.
+ */
+@XmlType(name = "bucket")
+public class BucketDTO {
+
+    private String id;
+    private String name;
+    private String description;
+    private Long created;
+
+    @ApiModelProperty("The bucket identifier")
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    @ApiModelProperty("The bucket name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty("The bucket description")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @ApiModelProperty("The created timestamp of this bucket")
+    public Long getCreated() {
+        return created;
+    }
+
+    public void setCreated(Long created) {
+        this.created = created;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
index 61b037f..597d700 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
@@ -28,6 +28,7 @@ public class ControllerConfigurationDTO {
 
     private Integer maxTimerDrivenThreadCount;
     private Integer maxEventDrivenThreadCount;
+    private String registryUrl;
 
     /**
      * @return maximum number of timer driven threads this NiFi has available

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
index 657d760..03e1a7d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
@@ -32,6 +32,7 @@ public class FlowConfigurationDTO {
     private Boolean supportsManagedAuthorizer;
     private Boolean supportsConfigurableAuthorizer;
     private Boolean supportsConfigurableUsersAndGroups;
+    private Boolean supportsFlowVersioning;
     private Long autoRefreshIntervalSeconds;
 
     private Date currentTime;
@@ -127,4 +128,18 @@ public class FlowConfigurationDTO {
     public void setTimeOffset(Integer timeOffset) {
         this.timeOffset = timeOffset;
     }
+
+    /**
+     * @return whether this NiFi is configured for support flow versioning
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi supports flow versioning."
+    )
+    public Boolean getSupportsFlowVersioning() {
+        return supportsFlowVersioning;
+    }
+
+    public void setSupportsFlowVersioning(Boolean supportsFlowVersioning) {
+        this.supportsFlowVersioning = supportsFlowVersioning;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RegistryDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RegistryDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RegistryDTO.java
new file mode 100644
index 0000000..f630430
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RegistryDTO.java
@@ -0,0 +1,69 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * Details about a configured registry.
+ */
+@XmlType(name = "registry")
+public class RegistryDTO {
+
+    private String id;
+    private String name;
+    private String description;
+    private String uri;
+
+    @ApiModelProperty("The registry identifier")
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    @ApiModelProperty("The registry name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty("The registry description")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @ApiModelProperty("The registry URI")
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
index 27a83e6..1e1f5f5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
@@ -28,6 +28,7 @@ public class VersionedFlowDTO {
     private String flowId;
     private String flowName;
     private String description;
+    private String comments;
 
     @ApiModelProperty("The ID of the registry that the flow is tracked to")
     public String getRegistryId() {
@@ -73,4 +74,13 @@ public class VersionedFlowDTO {
     public void setDescription(String description) {
         this.description = description;
     }
+
+    @ApiModelProperty("Comments for the changeset")
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/flow/FlowBreadcrumbDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/flow/FlowBreadcrumbDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/flow/FlowBreadcrumbDTO.java
index 170a30f..60af3fa 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/flow/FlowBreadcrumbDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/flow/FlowBreadcrumbDTO.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.web.api.dto.flow;
 
 import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 
 import javax.xml.bind.annotation.XmlType;
 
@@ -28,6 +29,7 @@ public class FlowBreadcrumbDTO {
 
     private String id;
     private String name;
+    private VersionControlInformationDTO versionControlInformation;
 
     /**
      * The id for this group.
@@ -60,4 +62,18 @@ public class FlowBreadcrumbDTO {
     public void setName(final String name) {
         this.name = name;
     }
+
+    /**
+     * @return the process group version control information or null if not version controlled
+     */
+    @ApiModelProperty(
+            value = "The process group version control information or null if not version controlled."
+    )
+    public VersionControlInformationDTO getVersionControlInformation() {
+        return versionControlInformation;
+    }
+
+    public void setVersionControlInformation(VersionControlInformationDTO versionControlInformation) {
+        this.versionControlInformation = versionControlInformation;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
new file mode 100644
index 0000000..3d99308
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
@@ -0,0 +1,39 @@
+/*
+ * 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.web.api.entity;
+
+import org.apache.nifi.web.api.dto.BucketDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a BucketDTO.
+ */
+@XmlRootElement(name = "bucketEntity")
+public class BucketEntity extends Entity {
+
+    private BucketDTO bucket;
+
+
+    public BucketDTO getBucket() {
+        return bucket;
+    }
+
+    public void setBucket(BucketDTO bucket) {
+        this.bucket = bucket;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketsEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketsEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketsEntity.java
new file mode 100644
index 0000000..830c1c2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketsEntity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.web.api.entity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a set of BucketEntity's.
+ */
+@XmlRootElement(name = "bucketsEntity")
+public class BucketsEntity extends Entity {
+
+    private Set<BucketEntity> buckets;
+
+    /**
+     * @return collection of BucketEntity's that are being serialized
+     */
+    public Set<BucketEntity> getBuckets() {
+        return buckets;
+    }
+
+    public void setBuckets(Set<BucketEntity> buckets) {
+        this.buckets = buckets;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
new file mode 100644
index 0000000..6705c7a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.web.api.entity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a set of RegistryEntity's.
+ */
+@XmlRootElement(name = "registriesEntity")
+public class RegistriesEntity extends Entity {
+
+    private Set<RegistryEntity> registries;
+
+    /**
+     * @return collection of LabelEntity's that are being serialized
+     */
+    public Set<RegistryEntity> getRegistries() {
+        return registries;
+    }
+
+    public void setRegistries(Set<RegistryEntity> registries) {
+        this.registries = registries;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
new file mode 100644
index 0000000..5968579
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
@@ -0,0 +1,39 @@
+/*
+ * 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.web.api.entity;
+
+import org.apache.nifi.web.api.dto.RegistryDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a RegistryDTO.
+ */
+@XmlRootElement(name = "registryEntity")
+public class RegistryEntity extends ComponentEntity {
+
+    private RegistryDTO component;
+
+
+    public RegistryDTO getComponent() {
+        return component;
+    }
+
+    public void setComponent(RegistryDTO component) {
+        this.component = component;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
index e8ec81f..749a118 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
@@ -25,16 +25,16 @@ import javax.xml.bind.annotation.XmlRootElement;
 
 @XmlRootElement(name = "versionControlInformationEntity")
 public class VersionControlInformationEntity extends Entity {
-    private VersionControlInformationDTO versionControlDto;
+    private VersionControlInformationDTO versionControlInformation;
     private RevisionDTO processGroupRevision;
 
     @ApiModelProperty("The Version Control information")
     public VersionControlInformationDTO getVersionControlInformation() {
-        return versionControlDto;
+        return versionControlInformation;
     }
 
     public void setVersionControlInformation(VersionControlInformationDTO versionControlDto) {
-        this.versionControlDto = versionControlDto;
+        this.versionControlInformation = versionControlDto;
     }
 
     @ApiModelProperty("The Revision for the Process Group")

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index 8934788..d335461 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -16,7 +16,6 @@
  */
 package org.apache.nifi.groups;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -42,7 +41,6 @@ import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.flow.UnknownResourceException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -941,6 +939,11 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
     void setVersionControlInformation(VersionControlInformation versionControlInformation, Map<String, String> versionedComponentIds);
 
     /**
+     * Disconnects this Process Group from version control. If not currently under version control, this method does nothing.
+     */
+    void disconnectVersionControl();
+
+    /**
      * Synchronizes the Process Group with the given Flow Registry, determining whether or not the local flow
      * is up to date with the newest version of the flow in the Registry and whether or not the local flow has been
      * modified since it was last synced with the Flow Registry. If this Process Group is not under Version Control,

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
index a5bb738..962a940 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -17,7 +17,11 @@
 
 package org.apache.nifi.registry.flow;
 
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.registry.bucket.Bucket;
+
 import java.io.IOException;
+import java.util.Set;
 
 public interface FlowRegistry {
 
@@ -27,6 +31,19 @@ public interface FlowRegistry {
     String getURL();
 
     /**
+     * @return the name of the Flow Registry
+     */
+    String getName();
+
+    /**
+     * Gets the buckets for the specified user.
+     *
+     * @param user current user
+     * @return buckets for this user
+     */
+    Set<Bucket> getBuckets(NiFiUser user) throws IOException;
+
+    /**
      * Registers the given Versioned Flow with the Flow Registry
      *
      * @param flow the Versioned Flow to add to the registry

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
index 09d032e..6548004 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
@@ -160,6 +160,11 @@
             <groupId>org.apache.nifi.registry</groupId>
             <artifactId>nifi-registry-flow-diff</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.curator</groupId>

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 282c50d..3b8117b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -16,30 +16,6 @@
  */
 package org.apache.nifi.groups;
 
-import static java.util.Objects.requireNonNull;
-
-import java.io.IOException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -133,6 +109,30 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+
 public final class StandardProcessGroup implements ProcessGroup {
 
     private final String id;
@@ -2835,6 +2835,16 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    public void disconnectVersionControl() {
+        writeLock.lock();
+        try {
+            // TODO remove version component ids from each component (until another versioned PG is encountered)
+            this.versionControlInfo.set(null);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
     private void updateVersionedComponentIds(final ProcessGroup processGroup, final Map<String, String> versionedComponentIds) {
         if (versionedComponentIds == null || versionedComponentIds.isEmpty()) {
             return;

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
index 22ba50b..2cc39c6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
@@ -17,6 +17,15 @@
 
 package org.apache.nifi.registry.flow;
 
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.registry.bucket.Bucket;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -35,13 +44,6 @@ import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.UUID;
 
-import org.codehaus.jackson.JsonFactory;
-import org.codehaus.jackson.JsonGenerator;
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.map.DeserializationConfig;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.codehaus.jackson.util.DefaultPrettyPrinter;
-
 /**
  * A simple file-based implementation of a Flow Registry Client. Rather than interacting
  * with an actual Flow Registry, this implementation simply reads flows from disk and writes
@@ -114,6 +116,35 @@ public class FileBasedFlowRegistryClient implements FlowRegistryClient, FlowRegi
     }
 
     @Override
+    public String getName() {
+        return "Local Registry";
+    }
+
+    @Override
+    public Set<Bucket> getBuckets(NiFiUser user) throws IOException {
+        final Set<Bucket> buckets = new HashSet<>();
+
+        final File[] bucketDirs = directory.listFiles();
+        if (bucketDirs == null) {
+            throw new IOException("Could not get listing of directory " + directory);
+        }
+
+        for (final File bucketDirectory : bucketDirs) {
+            final String bucketIdentifier = bucketDirectory.getName();
+            final long creation = bucketDirectory.lastModified();
+
+            final Bucket bucket = new Bucket();
+            bucket.setIdentifier(bucketIdentifier);
+            bucket.setName("Bucket '" + bucketIdentifier + "'");
+            bucket.setCreatedTimestamp(creation);
+
+            buckets.add(bucket);
+        }
+
+        return buckets;
+    }
+
+    @Override
     public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, UnknownResourceException {
         Objects.requireNonNull(flow);
         Objects.requireNonNull(flow.getBucketIdentifier());
@@ -303,9 +334,9 @@ public class FileBasedFlowRegistryClient implements FlowRegistryClient, FlowRegi
         final File contentsFile = new File(versionDir, "flow.xml");
 
         final VersionedProcessGroup processGroup;
-        try (final JsonParser parser = jsonFactory.createJsonParser(contentsFile)) {
+        try (final JsonParser parser = jsonFactory.createParser(contentsFile)) {
             final ObjectMapper mapper = new ObjectMapper();
-            mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
 
             parser.setCodec(mapper);
             processGroup = parser.readValueAs(VersionedProcessGroup.class);

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
index 27e1678..18dc51b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
@@ -17,16 +17,6 @@
 
 package org.apache.nifi.controller.service.mock;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-
 import org.apache.nifi.authorization.Resource;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.connectable.Connectable;
@@ -52,6 +42,16 @@ import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.remote.RemoteGroupPort;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
 public class MockProcessGroup implements ProcessGroup {
     private final Map<String, ControllerServiceNode> serviceMap = new HashMap<>();
     private final Map<String, ProcessorNode> processorMap = new HashMap<>();
@@ -661,4 +661,9 @@ public class MockProcessGroup implements ProcessGroup {
     public void setVersionControlInformation(VersionControlInformation versionControlInformation, Map<String, String> versionedComponentIds) {
         this.versionControlInfo = versionControlInformation;
     }
+
+    @Override
+    public void disconnectVersionControl() {
+        this.versionControlInfo = null;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 84e582c..a68ad0c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -16,14 +16,6 @@
  */
 package org.apache.nifi.web;
 
-import java.io.IOException;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-
 import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
@@ -31,7 +23,6 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.repository.claim.ContentDirection;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
-import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.UnknownResourceException;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
@@ -62,6 +53,7 @@ import org.apache.nifi.web.api.dto.PortDTO;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.ProcessorDTO;
 import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
+import org.apache.nifi.web.api.dto.RegistryDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
@@ -104,6 +96,7 @@ import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
+import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupPortEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
@@ -119,6 +112,14 @@ import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
 /**
  * Defines the NiFiServiceFacade interface.
  */
@@ -1320,6 +1321,14 @@ public interface NiFiServiceFacade {
     VersionControlInformationEntity setVersionControlInformation(Revision processGroupRevision, String processGroupId, VersionControlInformationDTO versionControlInfo,
         Map<String, String> versionedComponentMapping);
 
+    /**
+     * Disconnects the specified Process Group from version control.
+     *
+     * @param revision revision
+     * @param processGroupId group id
+     * @return version control information prior to disconnecting
+     */
+    VersionControlInformationEntity deleteVersionControl(final Revision revision, final String processGroupId);
 
     /**
      * Retrieves the Versioned Flow Snapshot for the coordinates provided by the given Version Control Information DTO
@@ -1381,8 +1390,6 @@ public interface NiFiServiceFacade {
     ProcessGroupEntity updateProcessGroup(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
         boolean verifyNotModified);
 
-    void setFlowRegistryClient(FlowRegistryClient flowRegistryClient);
-
     // ----------------------------------------
     // Component state methods
     // ----------------------------------------
@@ -1829,6 +1836,52 @@ public interface NiFiServiceFacade {
     void verifyDeleteReportingTask(String reportingTaskId);
 
     // ----------------------------------------
+    // Registry methods
+    // ----------------------------------------
+
+    /**
+     * Creates a registry.
+     *
+     * @param revision revision
+     * @param registryDTO The registry DTO
+     * @return The reporting task DTO
+     */
+    RegistryEntity createRegistry(Revision revision, RegistryDTO registryDTO);
+
+    /**
+     * Gets a registry with the specified id.
+     *
+     * @param registryId id
+     * @return entity
+     */
+    RegistryEntity getRegistry(String registryId);
+
+    /**
+     * Gets all registries.
+     *
+     * @return registries
+     */
+    Set<RegistryEntity> getRegistries();
+
+    /**
+     * Updates the specified registry using the specified revision.
+     *
+     * @param revision revision
+     * @param registryDTO the registry dto
+     * @return the updated registry registry entity
+     */
+    RegistryEntity updateRegistry(Revision revision, RegistryDTO registryDTO);
+
+    /**
+     * Deletes the specified registry using the specified revision.
+     *
+     * @param revision revision
+     * @param registryId id
+     * @return the deleted registry entity
+     */
+    RegistryEntity deleteRegistry(Revision revision, String registryId);
+
+    // ----------------------------------------
     // History methods
     // ----------------------------------------
     /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
index 0881cdf..065db86 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
@@ -98,6 +98,7 @@ public class NiFiWebApiResourceConfig extends ResourceConfig {
         register(ctx.getBean("accessResource"));
         register(ctx.getBean("accessPolicyResource"));
         register(ctx.getBean("tenantsResource"));
+        register(ctx.getBean("versionsResource"));
 
         // exception mappers
         register(AccessDeniedExceptionMapper.class);

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index d3a5fd0..0105bf1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,32 +16,7 @@
  */
 package org.apache.nifi.web;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
-
+import com.google.common.collect.Sets;
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
 import org.apache.nifi.action.FlowChangeAction;
@@ -141,6 +116,7 @@ import org.apache.nifi.reporting.BulletinQuery;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.reporting.ComponentType;
 import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.Tuple;
 import org.apache.nifi.web.api.dto.AccessPolicyDTO;
 import org.apache.nifi.web.api.dto.AccessPolicySummaryDTO;
 import org.apache.nifi.web.api.dto.AffectedComponentDTO;
@@ -179,6 +155,7 @@ import org.apache.nifi.web.api.dto.ProcessorConfigDTO;
 import org.apache.nifi.web.api.dto.ProcessorDTO;
 import org.apache.nifi.web.api.dto.PropertyDescriptorDTO;
 import org.apache.nifi.web.api.dto.PropertyHistoryDTO;
+import org.apache.nifi.web.api.dto.RegistryDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
@@ -236,6 +213,7 @@ import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
+import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupPortEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
@@ -279,7 +257,30 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Sets;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Implementation of NiFiServiceFacade that performs revision checking.
@@ -330,6 +331,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     private AuthorizableLookup authorizableLookup;
 
+    private Map<String, Tuple<Revision, RegistryDTO>> registryCache = new HashMap<>();
+
     // -----------------------------------------
     // Synchronization methods
     // -----------------------------------------
@@ -2257,6 +2260,45 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entityFactory.createControllerServiceEntity(snapshot, null, permissions, null);
     }
 
+    private RegistryEntity createRegistryEntity(final Revision updatedRevision, final RegistryDTO registryDTO) {
+        final RegistryEntity entity = new RegistryEntity();
+        entity.setId(registryDTO.getId());
+        entity.setPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getController()));
+        entity.setRevision(dtoFactory.createRevisionDTO(updatedRevision));
+        entity.setComponent(registryDTO);
+        return entity;
+    }
+
+    @Override
+    public RegistryEntity createRegistry(Revision revision, RegistryDTO registryDTO) {
+        registryCache.put(registryDTO.getId(), new Tuple(revision, registryDTO));
+        return createRegistryEntity(revision, registryDTO);
+    }
+
+    @Override
+    public RegistryEntity getRegistry(String registryId) {
+        final Tuple<Revision, RegistryDTO> registry = registryCache.get(registryId);
+        return createRegistry(registry.getKey(), registry.getValue());
+    }
+
+    @Override
+    public Set<RegistryEntity> getRegistries() {
+        return registryCache.values().stream()
+                .map(registry -> createRegistry(registry.getKey(), registry.getValue()))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public RegistryEntity updateRegistry(Revision revision, RegistryDTO registryDTO) {
+        registryCache.put(registryDTO.getId(), new Tuple(revision, registryDTO));
+        return createRegistryEntity(revision, registryDTO);
+    }
+
+    @Override
+    public RegistryEntity deleteRegistry(Revision revision, String registryId) {
+        final Tuple<Revision, RegistryDTO> registry = registryCache.remove(registryId);
+        return createRegistryEntity(registry.getKey(), registry.getValue());
+    }
 
     @Override
     public ReportingTaskEntity createReportingTask(final Revision revision, final ReportingTaskDTO reportingTaskDTO) {
@@ -3504,7 +3546,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             return null;
         }
 
-        final VersionControlInformationDTO versionControlDto = dtoFactory.createVersionControlInformationDto(versionControlInfo);
+        final VersionControlInformationDTO versionControlDto = dtoFactory.createVersionControlInformationDto(processGroup);
         final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(groupId));
         return entityFactory.createVersionControlInformationEntity(versionControlDto, groupRevision);
     }
@@ -3555,7 +3597,19 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final RevisionUpdate<VersionControlInformationDTO> snapshot = updateComponent(revision,
             group,
             () -> processGroupDAO.updateVersionControlInformation(versionControlInfo, versionedComponentMapping),
-            processGroup -> dtoFactory.createVersionControlInformationDto(processGroup.getVersionControlInformation()));
+            processGroup -> dtoFactory.createVersionControlInformationDto(processGroup));
+
+        return entityFactory.createVersionControlInformationEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()));
+    }
+
+    @Override
+    public VersionControlInformationEntity deleteVersionControl(final Revision revision, final String processGroupId) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
+
+        final RevisionUpdate<VersionControlInformationDTO> snapshot = updateComponent(revision,
+            group,
+            () -> processGroupDAO.disconnectVersionControl(processGroupId),
+            processGroup -> dtoFactory.createVersionControlInformationDto(group));
 
         return entityFactory.createVersionControlInformationEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()));
     }
@@ -3835,12 +3889,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entityFactory.createProcessGroupEntity(snapshot.getComponent(), updatedRevision, permissions, status, bulletinEntities);
     }
 
-
-    @Override
-    public void setFlowRegistryClient(final FlowRegistryClient client) {
-        this.flowRegistryClient = client;
-    }
-
     private AuthorizationResult authorizeAction(final Action action) {
         final String sourceId = action.getSourceId();
         final Component type = action.getSourceType();
@@ -4194,4 +4242,28 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     public void setLeaderElectionManager(final LeaderElectionManager leaderElectionManager) {
         this.leaderElectionManager = leaderElectionManager;
     }
+
+    public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
+        this.flowRegistryClient = flowRegistryClient;
+
+        // temp code to load the registry client cache
+        final Set<String> registryIdentifiers = flowRegistryClient.getRegistryIdentifiers();
+        if (registryIdentifiers != null) {
+
+            for (final String registryIdentifier : registryIdentifiers) {
+                final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryIdentifier);
+
+                final RegistryDTO registry = new RegistryDTO();
+                registry.setId(registryIdentifier);
+                registry.setName(flowRegistry.getName());
+                registry.setUri(flowRegistry.getURL());
+                registry.setDescription("Default client for storing Flow Revisions to the local disk.");
+
+                final RegistryEntity registryEntity = new RegistryEntity();
+                registryEntity.setComponent(registry);
+
+                registryCache.put(registryIdentifier, new Tuple(new Revision(0L, null, registryIdentifier), registry));
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
index b213755..959d06d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
@@ -37,6 +37,7 @@ import org.apache.nifi.web.api.dto.BulletinDTO;
 import org.apache.nifi.web.api.dto.ClusterDTO;
 import org.apache.nifi.web.api.dto.ControllerServiceDTO;
 import org.apache.nifi.web.api.dto.NodeDTO;
+import org.apache.nifi.web.api.dto.RegistryDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ClusterEntity;
@@ -45,12 +46,16 @@ import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.Entity;
 import org.apache.nifi.web.api.entity.HistoryEntity;
 import org.apache.nifi.web.api.entity.NodeEntity;
+import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
+import org.apache.nifi.web.api.request.ClientIdParameter;
 import org.apache.nifi.web.api.request.DateTimeParameter;
+import org.apache.nifi.web.api.request.LongParameter;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.HttpMethod;
 import javax.ws.rs.POST;
@@ -82,6 +87,17 @@ public class ControllerResource extends ApplicationResource {
     private ControllerServiceResource controllerServiceResource;
 
     /**
+     * Populate the uri's for the specified registry.
+     *
+     * @param registryEntity registry
+     * @return dtos
+     */
+    public RegistryEntity populateRemainingRegistryEntityContent(final RegistryEntity registryEntity) {
+        registryEntity.setUri(generateResourceUri("controller", "registries", registryEntity.getId()));
+        return registryEntity;
+    }
+
+    /**
      * Authorizes access to the flow.
      */
     private void authorizeController(final RequestAction action) {
@@ -290,6 +306,288 @@ public class ControllerResource extends ApplicationResource {
         );
     }
 
+    // ----------
+    // registries
+    // ----------
+
+    /**
+     * Creates a new Registry.
+     *
+     * @param httpServletRequest  request
+     * @param requestRegistryEntity A registryEntity.
+     * @return A registryEntity.
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("registries")
+    @ApiOperation(
+            value = "Creates a new registry",
+            response = RegistryEntity.class,
+            authorizations = {
+                    @Authorization(value = "Write - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                    @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                    @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+
+    public Response createRegistry(
+            @Context final HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The registry configuration details.",
+                    required = true
+            ) final RegistryEntity requestRegistryEntity) {
+
+        if (requestRegistryEntity == null || requestRegistryEntity.getComponent() == null) {
+            throw new IllegalArgumentException("Registry details must be specified.");
+        }
+
+        if (requestRegistryEntity.getRevision() == null || (requestRegistryEntity.getRevision().getVersion() == null || requestRegistryEntity.getRevision().getVersion() != 0)) {
+            throw new IllegalArgumentException("A revision of 0 must be specified when creating a new Registry.");
+        }
+
+        final RegistryDTO requestReportingTask = requestRegistryEntity.getComponent();
+        if (requestReportingTask.getId() != null) {
+            throw new IllegalArgumentException("Registry ID cannot be specified.");
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST, requestRegistryEntity);
+        }
+
+        return withWriteLock(
+                serviceFacade,
+                requestRegistryEntity,
+                lookup -> {
+                    authorizeController(RequestAction.WRITE);
+                },
+                null,
+                (registryEntity) -> {
+                    final RegistryDTO registry = registryEntity.getComponent();
+
+                    // set the processor id as appropriate
+                    registry.setId(generateUuid());
+
+                    // create the reporting task and generate the json
+                    final Revision revision = getRevision(registryEntity, registry.getId());
+                    final RegistryEntity entity = serviceFacade.createRegistry(revision, registry);
+                    populateRemainingRegistryEntityContent(entity);
+
+                    // build the response
+                    return generateCreatedResponse(URI.create(entity.getUri()), entity).build();
+                }
+        );
+    }
+
+    /**
+     * Retrieves the specified registry.
+     *
+     * @param id The id of the registry to retrieve
+     * @return A registryEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/registries/{id}")
+    @ApiOperation(
+            value = "Gets a registry",
+            response = RegistryEntity.class,
+            authorizations = {
+                    @Authorization(value = "Read - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                    @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                    @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                    @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response getRegistry(
+            @ApiParam(
+                    value = "The registry id.",
+                    required = true
+            )
+            @PathParam("id") final String id) {
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        // authorize access
+        authorizeController(RequestAction.READ);
+
+        // get the registry
+        final RegistryEntity entity = serviceFacade.getRegistry(id);
+        populateRemainingRegistryEntityContent(entity);
+
+        return generateOkResponse(entity).build();
+    }
+
+    /**
+     * Updates the specified registry.
+     *
+     * @param httpServletRequest      request
+     * @param id                      The id of the controller service to update.
+     * @param requestRegsitryEntity A controllerServiceEntity.
+     * @return A controllerServiceEntity.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/registries/{id}")
+    @ApiOperation(
+            value = "Updates a registry",
+            response = RegistryEntity.class,
+            authorizations = {
+                    @Authorization(value = "Write - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                    @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                    @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                    @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response updateControllerService(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The registry id.",
+                    required = true
+            )
+            @PathParam("id") final String id,
+            @ApiParam(
+                    value = "The registry configuration details.",
+                    required = true
+            ) final RegistryEntity requestRegsitryEntity) {
+
+        if (requestRegsitryEntity == null || requestRegsitryEntity.getComponent() == null) {
+            throw new IllegalArgumentException("Registry details must be specified.");
+        }
+
+        if (requestRegsitryEntity.getRevision() == null) {
+            throw new IllegalArgumentException("Revision must be specified.");
+        }
+
+        // ensure the ids are the same
+        final RegistryDTO requestRegistryDTO = requestRegsitryEntity.getComponent();
+        if (!id.equals(requestRegistryDTO.getId())) {
+            throw new IllegalArgumentException(String.format("The registry id (%s) in the request body does not equal the "
+                    + "registry id of the requested resource (%s).", requestRegistryDTO.getId(), id));
+        }
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.PUT, requestRegsitryEntity);
+        }
+
+        // handle expects request (usually from the cluster manager)
+        final Revision requestRevision = getRevision(requestRegsitryEntity, id);
+        return withWriteLock(
+                serviceFacade,
+                requestRegsitryEntity,
+                requestRevision,
+                lookup -> {
+                    authorizeController(RequestAction.WRITE);
+                },
+                null,
+                (revision, registryEntity) -> {
+                    final RegistryDTO registry = registryEntity.getComponent();
+
+                    // update the controller service
+                    final RegistryEntity entity = serviceFacade.updateRegistry(revision, registry);
+                    populateRemainingRegistryEntityContent(entity);
+
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
+    /**
+     * Removes the specified registry.
+     *
+     * @param httpServletRequest request
+     * @param version            The revision is used to verify the client is working with
+     *                           the latest version of the flow.
+     * @param clientId           Optional client id. If the client id is not specified, a
+     *                           new one will be generated. This value (whether specified or generated) is
+     *                           included in the response.
+     * @param id                 The id of the registry to remove.
+     * @return A entity containing the client id and an updated revision.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/registries/{id}")
+    @ApiOperation(
+            value = "Deletes a reistry",
+            response = RegistryEntity.class,
+            authorizations = {
+                    @Authorization(value = "Write - /controller")
+            }
+    )
+    @ApiResponses(
+            value = {
+                    @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                    @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                    @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                    @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                    @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response deleteRegistry(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The revision is used to verify the client is working with the latest version of the flow.",
+                    required = false
+            )
+            @QueryParam(VERSION) final LongParameter version,
+            @ApiParam(
+                    value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
+                    required = false
+            )
+            @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) final ClientIdParameter clientId,
+            @ApiParam(
+                    value = "The registry id.",
+                    required = true
+            )
+            @PathParam("id") final String id) {
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.DELETE);
+        }
+
+        final RegistryEntity requestRegistryEntity = new RegistryEntity();
+        requestRegistryEntity.setId(id);
+
+        // handle expects request (usually from the cluster manager)
+        final Revision requestRevision = new Revision(version == null ? null : version.getLong(), clientId.getClientId(), id);
+        return withWriteLock(
+                serviceFacade,
+                requestRegistryEntity,
+                requestRevision,
+                lookup -> {
+                    authorizeController(RequestAction.WRITE);
+                },
+                null,
+                (revision, registryEntity) -> {
+                    // delete the specified registry
+                    final RegistryEntity entity = serviceFacade.deleteRegistry(revision, registryEntity.getId());
+                    return generateOkResponse(entity).build();
+                }
+        );
+    }
+
     /**
      * Creates a Bulletin.
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index 38e8891..3e9be6d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -39,13 +39,18 @@ import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.nar.NarClassLoaders;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.IllegalClusterResourceRequestException;
+import org.apache.nifi.web.NiFiCoreException;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.dto.AboutDTO;
 import org.apache.nifi.web.api.dto.BannerDTO;
+import org.apache.nifi.web.api.dto.BucketDTO;
 import org.apache.nifi.web.api.dto.BulletinBoardDTO;
 import org.apache.nifi.web.api.dto.BulletinQueryDTO;
 import org.apache.nifi.web.api.dto.ClusterDTO;
@@ -64,6 +69,8 @@ import org.apache.nifi.web.api.entity.AboutEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
 import org.apache.nifi.web.api.entity.BannerEntity;
+import org.apache.nifi.web.api.entity.BucketEntity;
+import org.apache.nifi.web.api.entity.BucketsEntity;
 import org.apache.nifi.web.api.entity.BulletinBoardEntity;
 import org.apache.nifi.web.api.entity.ClusteSummaryEntity;
 import org.apache.nifi.web.api.entity.ClusterSearchResultsEntity;
@@ -84,6 +91,8 @@ import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorTypesEntity;
+import org.apache.nifi.web.api.entity.RegistriesEntity;
+import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskTypesEntity;
@@ -112,6 +121,7 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.EnumSet;
@@ -148,8 +158,11 @@ public class FlowResource extends ApplicationResource {
     private TemplateResource templateResource;
     private ProcessGroupResource processGroupResource;
     private ControllerServiceResource controllerServiceResource;
+    private ControllerResource controllerResource;
     private ReportingTaskResource reportingTaskResource;
 
+    private FlowRegistryClient flowRegistryClient;
+
     public FlowResource() {
         super();
     }
@@ -1317,6 +1330,98 @@ public class FlowResource extends ApplicationResource {
         return generateOkResponse(entity).build();
     }
 
+    // ----------
+    // registries
+    // ----------
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("registries")
+    @ApiOperation(value = "Gets the listing of available registries", response = RegistriesEntity.class, authorizations = {
+            @Authorization(value = "Read - /flow")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+            @ApiResponse(code = 401, message = "Client could not be authenticated."),
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 404, message = "The specified resource could not be found."),
+            @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getRegistries() {
+        authorizeFlow();
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        final Set<RegistryEntity> registries = serviceFacade.getRegistries();
+        registries.forEach(registry -> controllerResource.populateRemainingRegistryEntityContent(registry));
+
+        final RegistriesEntity registryEntities = new RegistriesEntity();
+        registryEntities.setRegistries(registries);
+
+        return generateOkResponse(registryEntities).build();
+    }
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("registries/{id}/buckets")
+    @ApiOperation(value = "Gets the buckets from the specified registry for the current user", response = BucketsEntity.class, authorizations = {
+            @Authorization(value = "Read - /flow")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+            @ApiResponse(code = 401, message = "Client could not be authenticated."),
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 404, message = "The specified resource could not be found."),
+            @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getBuckets(
+            @ApiParam(
+                    value = "The registry id.",
+                    required = true
+            )
+            @PathParam("id") String id) {
+
+        authorizeFlow();
+
+        try {
+            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(id);
+            if (flowRegistry == null) {
+                throw new IllegalArgumentException("The specified registry id is unknown to this NiFi.");
+            }
+
+            final Set<Bucket> userBuckets = flowRegistry.getBuckets(NiFiUserUtils.getNiFiUser());
+
+            final BucketsEntity bucketsEntity = new BucketsEntity();
+
+            if (userBuckets != null) {
+
+                final Set<BucketEntity> bucketSet = new HashSet<>();
+                for (final Bucket userBucket : userBuckets) {
+                    final BucketDTO bucket = new BucketDTO();
+                    bucket.setId(userBucket.getIdentifier());
+                    bucket.setName(userBucket.getName());
+                    bucket.setDescription(userBucket.getDescription());
+                    bucket.setCreated(userBucket.getCreatedTimestamp());
+
+                    final BucketEntity bucketEntity = new BucketEntity();
+                    bucketEntity.setBucket(bucket);
+
+                    bucketSet.add(bucketEntity);
+                }
+
+                bucketsEntity.setBuckets(bucketSet);
+            }
+
+            return generateOkResponse(bucketsEntity).build();
+        } catch (final IOException ioe) {
+            throw new NiFiCoreException("Unable to obtain bucket listing: " + ioe.getMessage(), ioe);
+        }
+    }
+
     // --------------
     // bulletin board
     // --------------
@@ -2524,6 +2629,10 @@ public class FlowResource extends ApplicationResource {
         this.processGroupResource = processGroupResource;
     }
 
+    public void setControllerResource(ControllerResource controllerResource) {
+        this.controllerResource = controllerResource;
+    }
+
     public void setControllerServiceResource(ControllerServiceResource controllerServiceResource) {
         this.controllerServiceResource = controllerServiceResource;
     }
@@ -2535,4 +2644,8 @@ public class FlowResource extends ApplicationResource {
     public void setAuthorizer(Authorizer authorizer) {
         this.authorizer = authorizer;
     }
+
+    public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
+        this.flowRegistryClient = flowRegistryClient;
+    }
 }


[48/50] nifi git commit: NIFI-4436: Fixed bug that caused a Process Group to be 'dirty' if a processor that was referencing a non-existent controller service is updated to reference an externally available controller service

Posted by bb...@apache.org.
NIFI-4436: Fixed bug that caused a Process Group to be 'dirty' if a processor that was referencing a non-existent controller service is updated to reference an externally available controller service

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/f702f808
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/f702f808
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/f702f808

Branch: refs/heads/master
Commit: f702f808a70929980f862ef62c2359b73f5e6b6b
Parents: b3e1584
Author: Mark Payne <ma...@hotmail.com>
Authored: Wed Jan 3 15:29:39 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:57 2018 -0500

----------------------------------------------------------------------
 .../nifi/controller/StandardFlowSynchronizer.java       |  2 +-
 .../org/apache/nifi/groups/StandardProcessGroup.java    | 12 +++++++++++-
 .../org/apache/nifi/web/StandardNiFiServiceFacade.java  | 12 +++++++++++-
 3 files changed, 23 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/f702f808/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index 9bb3d2f..425110c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -240,7 +240,7 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
                     existingFlowEmpty = taskElements.isEmpty()
                         && unrootedControllerServiceElements.isEmpty()
                         && isEmpty(rootGroupDto)
-                        && registriesPresent;
+                        && !registriesPresent;
                     logger.debug("Existing Flow Empty = {}", existingFlowEmpty);
                 }
             }

http://git-wip-us.apache.org/repos/asf/nifi/blob/f702f808/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 870804d..9418f40 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -3316,7 +3316,17 @@ public final class StandardProcessGroup implements ProcessGroup {
             ancestorServiceIds = Collections.emptySet();
         } else {
             ancestorServiceIds = parentGroup.getControllerServices(true).stream()
-                .map(ControllerServiceNode::getIdentifier)
+                .map(cs -> {
+                    // We want to map the Controller Service to its Versioned Component ID, if it has one.
+                    // If it does not have one, we want to generate it in the same way that our Flow Mapper does
+                    // because this allows us to find the Controller Service when doing a Flow Diff.
+                    final Optional<String> versionedId = cs.getVersionedComponentId();
+                    if (versionedId.isPresent()) {
+                        return versionedId.get();
+                    }
+
+                    return UUID.nameUUIDFromBytes(cs.getIdentifier().getBytes(StandardCharsets.UTF_8)).toString();
+                })
                 .collect(Collectors.toSet());
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/f702f808/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index b7559ec..1ccced2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -3824,7 +3824,17 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             ancestorServiceIds = Collections.emptySet();
         } else {
             ancestorServiceIds = parentGroup.getControllerServices(true).stream()
-                .map(ControllerServiceNode::getIdentifier)
+                .map(cs -> {
+                    // We want to map the Controller Service to its Versioned Component ID, if it has one.
+                    // If it does not have one, we want to generate it in the same way that our Flow Mapper does
+                    // because this allows us to find the Controller Service when doing a Flow Diff.
+                    final Optional<String> versionedId = cs.getVersionedComponentId();
+                    if (versionedId.isPresent()) {
+                        return versionedId.get();
+                    }
+
+                    return UUID.nameUUIDFromBytes(cs.getIdentifier().getBytes(StandardCharsets.UTF_8)).toString();
+                })
                 .collect(Collectors.toSet());
         }
 


[16/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
NIFI-4436:
- Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation
- Checkpoint: Can place flow under version control and can determine if modified
- Checkpoint: Change version working in some cases. Does not work if processor removed because COMPONENT_REMOVED type has ComponentA whose ID is the VersionedComponentID but we are trying to call ProcessorDAO.get() with this ID
- Checkpoint: Able to change flow from Version 1 to Version 2 and back. Not yet tested with controller services. Have not tried changing/removing connections. Not cluster-friendly yet. All inline, not in background. Have not taken into account ports, funnels, remote ports, etc. Have not tested with Labels yet
- Checkpoint after implementing ClusterReplicationComponentLifecycle instead of JerseyClientComponentLifecycle
- Checkpoint: Updated to allow starting version control and updating version in clustered mode
- Checkpoint: Updated versioning endpoint so that when version of a flow is updated, the bundle information is populated and the snapshot is replicated to the cluster.
- Checkpoint: Implemented endpoint for reverting to previously sync'ed version of a flow and updated version control endpoint so that Process Group can be pushed as a new version to existing flow instead of only creating a new flow
- Checkpoint: Updated so that if a Process Group is under Version Control and it has a child Process Group, which is also under Version Control, we can handle that gracefully. Not yet tested because it depends on updates to the nifi-registry module, which can't be compiled due to maven dependency conflicts


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/6a58d780
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/6a58d780
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/6a58d780

Branch: refs/heads/master
Commit: 6a58d780d7041eb2b29d4da7a4c60c525d63f66d
Parents: 8d4fe38
Author: Mark Payne <ma...@hotmail.com>
Authored: Thu Aug 10 10:02:35 2017 -0400
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:52 2018 -0500

----------------------------------------------------------------------
 .../nifi/components/VersionedComponent.java     |   39 +
 .../nifi-framework/nifi-client-dto/pom.xml      |    4 +
 .../nifi/web/api/dto/AffectedComponentDTO.java  |   31 +-
 .../apache/nifi/web/api/dto/ComponentDTO.java   |   10 +
 .../apache/nifi/web/api/dto/ConnectableDTO.java |   11 +
 .../nifi/web/api/dto/ProcessGroupDTO.java       |   12 +-
 .../web/api/dto/RemoteProcessGroupPortDTO.java  |   10 +
 .../api/dto/VersionControlInformationDTO.java   |   98 ++
 .../nifi/web/api/dto/VersionedFlowDTO.java      |   76 +
 .../api/dto/VersionedFlowUpdateRequestDTO.java  |   90 ++
 .../web/api/entity/AffectedComponentEntity.java |    4 +-
 .../VersionControlComponentMappingEntity.java   |   59 +
 .../entity/VersionControlInformationEntity.java |   48 +
 .../web/api/entity/VersionedFlowEntity.java     |   48 +
 .../api/entity/VersionedFlowSnapshotEntity.java |   58 +
 .../VersionedFlowUpdateRequestEntity.java       |   49 +
 .../endpoints/ControllerEndpointMerger.java     |    2 -
 .../FlowConfigurationEndpointMerger.java        |    1 -
 ...VersionControlInformationEndpointMerger.java |   56 +
 .../http/replication/RequestReplicator.java     |   77 +-
 .../ThreadPoolRequestReplicator.java            |   29 +-
 .../cluster/manager/ConnectionEntityMerger.java |    1 -
 .../nifi/cluster/manager/PortEntityMerger.java  |    1 -
 .../manager/ProcessGroupEntityMerger.java       |   17 +
 .../cluster/manager/ProcessorEntityMerger.java  |    1 -
 .../VersionControlInformationEntityMerger.java  |   48 +
 ...FileBasedClusterNodeFirewallFactoryBean.java |    6 +-
 .../TestThreadPoolRequestReplicator.java        |    7 +-
 .../apache/nifi/cluster/integration/Node.java   |    3 +-
 .../nifi-framework-core-api/pom.xml             |    4 +
 .../apache/nifi/connectable/Connectable.java    |    4 +-
 .../org/apache/nifi/connectable/Connection.java |    3 +-
 .../apache/nifi/controller/AbstractPort.java    |   25 +
 .../apache/nifi/controller/StandardFunnel.java  |   25 +
 .../org/apache/nifi/controller/label/Label.java |    4 +-
 .../service/ControllerServiceNode.java          |    3 +-
 .../org/apache/nifi/groups/ProcessGroup.java    |   57 +-
 .../apache/nifi/groups/RemoteProcessGroup.java  |    3 +-
 .../RemoteProcessGroupPortDescriptor.java       |    5 +
 .../apache/nifi/registry/flow/FlowRegistry.java |   93 ++
 .../nifi/registry/flow/FlowRegistryClient.java  |   37 +
 .../registry/flow/UnknownResourceException.java |   33 +
 .../flow/VersionControlInformation.java         |   69 +
 .../nifi-framework/nifi-framework-core/pom.xml  |    8 +
 .../nifi/connectable/StandardConnection.java    |   25 +
 .../apache/nifi/controller/FlowController.java  |   39 +-
 .../controller/StandardFlowSynchronizer.java    |   26 +
 .../nifi/controller/StandardProcessorNode.java  |   24 +
 .../nifi/controller/label/StandardLabel.java    |   32 +
 .../serialization/FlowFromDOMFactory.java       |   26 +
 .../serialization/StandardFlowSerializer.java   |   33 +
 .../service/ControllerServiceLoader.java        |    1 +
 .../service/StandardControllerServiceNode.java  |   24 +
 .../nifi/fingerprint/FingerprintFactory.java    |   21 +-
 .../nifi/groups/StandardProcessGroup.java       | 1181 ++++++++++++++-
 .../flow/FileBasedFlowRegistryClient.java       |  404 +++++
 .../flow/StandardVersionControlInformation.java |   89 ++
 .../InstantiatedConnectableComponent.java       |   40 +
 .../mapping/InstantiatedVersionedComponent.java |   24 +
 .../InstantiatedVersionedConnection.java        |   40 +
 .../InstantiatedVersionedControllerService.java |   40 +
 .../mapping/InstantiatedVersionedFunnel.java    |   40 +
 .../mapping/InstantiatedVersionedLabel.java     |   40 +
 .../flow/mapping/InstantiatedVersionedPort.java |   40 +
 .../InstantiatedVersionedProcessGroup.java      |   40 +
 .../mapping/InstantiatedVersionedProcessor.java |   40 +
 .../InstantiatedVersionedRemoteGroupPort.java   |   40 +
 ...InstantiatedVersionedRemoteProcessGroup.java |   40 +
 .../flow/mapping/NiFiRegistryDtoMapper.java     |  327 ++++
 .../flow/mapping/NiFiRegistryFlowMapper.java    |  397 +++++
 .../nifi/remote/StandardRemoteProcessGroup.java |   24 +
 ...tandardRemoteProcessGroupPortDescriptor.java |   10 +
 .../nifi/spring/FlowControllerFactoryBean.java  |   13 +-
 .../src/main/resources/FlowConfiguration.xsd    |   21 +-
 .../src/main/resources/nifi-context.xml         |    6 +
 .../controller/StandardFlowServiceTest.java     |    3 +-
 .../nifi/controller/TestFlowController.java     |    7 +-
 .../reporting/TestStandardReportingContext.java |    5 +-
 .../scheduling/TestProcessorLifecycle.java      |    4 +-
 .../StandardFlowSerializerTest.java             |    4 +-
 .../service/mock/MockProcessGroup.java          |   36 +
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  187 ++-
 .../apache/nifi/web/NiFiServiceFacadeLock.java  |    6 +
 .../nifi/web/StandardNiFiServiceFacade.java     |  536 ++++++-
 .../nifi/web/api/ApplicationResource.java       |   80 +-
 .../org/apache/nifi/web/api/FlowResource.java   |   18 +
 .../nifi/web/api/ProcessGroupResource.java      |    4 +-
 .../apache/nifi/web/api/VersionsResource.java   | 1409 ++++++++++++++++++
 .../web/api/concurrent/AsyncRequestManager.java |  162 ++
 .../api/concurrent/AsynchronousWebRequest.java  |   80 +
 .../nifi/web/api/concurrent/RequestManager.java |   69 +
 .../StandardAsynchronousWebRequest.java         |   93 ++
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  244 ++-
 .../apache/nifi/web/api/dto/EntityFactory.java  |    8 +
 .../nifi/web/controller/ControllerFacade.java   |    6 +
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |   24 +
 .../web/dao/impl/StandardProcessGroupDAO.java   |  117 +-
 .../nifi/web/util/AffectedComponentUtils.java   |   71 +
 .../nifi/web/util/CancellableTimedPause.java    |   59 +
 .../ClusterReplicationComponentLifecycle.java   |  445 ++++++
 .../nifi/web/util/ComponentLifecycle.java       |   62 +
 .../web/util/LifecycleManagementException.java  |   34 +
 .../nifi/web/util/LocalComponentLifecycle.java  |  312 ++++
 .../src/main/resources/nifi-web-api-context.xml |   26 +
 .../nifi/web/revision/NaiveRevisionManager.java |    3 +-
 pom.xml                                         |   11 +
 106 files changed, 8319 insertions(+), 222 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-api/src/main/java/org/apache/nifi/components/VersionedComponent.java
----------------------------------------------------------------------
diff --git a/nifi-api/src/main/java/org/apache/nifi/components/VersionedComponent.java b/nifi-api/src/main/java/org/apache/nifi/components/VersionedComponent.java
new file mode 100644
index 0000000..164a4f2
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/components/VersionedComponent.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components;
+
+import java.util.Optional;
+
+public interface VersionedComponent {
+
+    /**
+     * @return the unique identifier that maps this component to a component that is versioned
+     *         in a Flow Registry, or <code>Optional.empty</code> if this component has not been saved to a Flow Registry.
+     */
+    Optional<String> getVersionedComponentId();
+
+    /**
+     * Updates the versioned component identifier
+     *
+     * @param versionedComponentId the identifier of the versioned component
+     *
+     * @throws IllegalStateException if this component is already under version control with a different ID and
+     *             the given ID is not null
+     */
+    void setVersionedComponentId(String versionedComponentId);
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml
index b47b2bc..69bb4a0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml
@@ -26,5 +26,9 @@
             <groupId>io.swagger</groupId>
             <artifactId>swagger-annotations</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-data-model</artifactId>
+        </dependency>
     </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
index 3567b53..95024ca 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
@@ -26,6 +26,8 @@ import java.util.Collection;
 public class AffectedComponentDTO {
     public static final String COMPONENT_TYPE_PROCESSOR = "PROCESSOR";
     public static final String COMPONENT_TYPE_CONTROLLER_SERVICE = "CONTROLLER_SERVICE";
+    public static final String COMPONENT_TYPE_REMOTE_INPUT_PORT = "REMOTE_INPUT_PORT";
+    public static final String COMPONENT_TYPE_REMOTE_OUTPUT_PORT = "REMOTE_OUTPUT_PORT";
 
     private String processGroupId;
     private String id;
@@ -54,7 +56,9 @@ public class AffectedComponentDTO {
         this.id = id;
     }
 
-    @ApiModelProperty(value = "The type of this component", allowableValues = COMPONENT_TYPE_PROCESSOR + "," + COMPONENT_TYPE_CONTROLLER_SERVICE)
+    @ApiModelProperty(value = "The type of this component",
+        allowableValues = COMPONENT_TYPE_PROCESSOR + "," + COMPONENT_TYPE_CONTROLLER_SERVICE + ", "
+            + COMPONENT_TYPE_REMOTE_INPUT_PORT + ", " + COMPONENT_TYPE_REMOTE_OUTPUT_PORT)
     public String getReferenceType() {
         return referenceType;
     }
@@ -73,21 +77,6 @@ public class AffectedComponentDTO {
     }
 
     /**
-     * @return scheduled state of the processor referencing a controller service. If this component is another service, this field represents the controller service state
-     */
-    @ApiModelProperty(
-            value = "The scheduled state of a processor or reporting task referencing a controller service. If this component is another controller "
-                    + "service, this field represents the controller service state."
-    )
-    public String getState() {
-        return state;
-    }
-
-    public void setState(String state) {
-        this.state = state;
-    }
-
-    /**
      * @return active thread count for the referencing component
      */
     @ApiModelProperty(
@@ -114,4 +103,14 @@ public class AffectedComponentDTO {
     public void setValidationErrors(Collection<String> validationErrors) {
         this.validationErrors = validationErrors;
     }
+
+    @ApiModelProperty("The scheduled state of a processor or reporting task referencing a controller service. If this component is another controller "
+        + "service, this field represents the controller service state.")
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDTO.java
index 2feefd7..81915ee 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ComponentDTO.java
@@ -27,6 +27,7 @@ import javax.xml.bind.annotation.XmlType;
 public class ComponentDTO {
 
     private String id;
+    private String versionedComponentId;
 
     private String parentGroupId;
     private PositionDTO position;
@@ -47,6 +48,15 @@ public class ComponentDTO {
         this.id = id;
     }
 
+    @ApiModelProperty("The ID of the corresponding component that is under version control")
+    public String getVersionedComponentId() {
+        return versionedComponentId;
+    }
+
+    public void setVersionedComponentId(final String id) {
+        this.versionedComponentId = id;
+    }
+
     /**
      * @return id for the parent group of this component if applicable, null otherwise
      */

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectableDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectableDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectableDTO.java
index b820479..a63872e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectableDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectableDTO.java
@@ -27,6 +27,7 @@ import javax.xml.bind.annotation.XmlType;
 public class ConnectableDTO {
 
     private String id;
+    private String versionedComponentId;
     private String type;
     private String groupId;
     private String name;
@@ -50,6 +51,16 @@ public class ConnectableDTO {
         this.id = id;
     }
 
+    @ApiModelProperty("The ID of the corresponding component that is under version control")
+    public String getVersionedComponentId() {
+        return versionedComponentId;
+    }
+
+    public void setVersionedComponentId(final String id) {
+        this.versionedComponentId = id;
+    }
+
+
     /**
      * @return type of this connectable component
      */

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
index c8e4a39..7faf10b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
@@ -30,6 +30,7 @@ public class ProcessGroupDTO extends ComponentDTO {
     private String name;
     private String comments;
     private Map<String, String> variables;
+    private VersionControlInformationDTO versionControlInfo;
 
     private Integer runningCount;
     private Integer stoppedCount;
@@ -203,7 +204,6 @@ public class ProcessGroupDTO extends ComponentDTO {
         this.inactiveRemotePortCount = inactiveRemotePortCount;
     }
 
-
     @ApiModelProperty(value = "The variables that are configured for the Process Group. Note that this map contains only "
         + "those variables that are defined on this Process Group and not any variables that are defined in the parent "
         + "Process Group, etc. I.e., this Map will not contain all variables that are accessible by components in this "
@@ -215,4 +215,14 @@ public class ProcessGroupDTO extends ComponentDTO {
     public void setVariables(final Map<String, String> variables) {
         this.variables = variables;
     }
+
+    @ApiModelProperty("The Version Control information that indicates which Flow Registry, and where in the Flow Registry, "
+        + "this Process Group is tracking to; or null if this Process Group is not under version control")
+    public VersionControlInformationDTO getVersionControlInformation() {
+        return versionControlInfo;
+    }
+
+    public void setVersionControlInformation(final VersionControlInformationDTO versionControlInfo) {
+        this.versionControlInfo = versionControlInfo;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RemoteProcessGroupPortDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RemoteProcessGroupPortDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RemoteProcessGroupPortDTO.java
index 59c5631..8b0ddb4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RemoteProcessGroupPortDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/RemoteProcessGroupPortDTO.java
@@ -27,6 +27,7 @@ public class RemoteProcessGroupPortDTO {
 
     private String id;
     private String targetId;
+    private String versionedComponentId;
     private String groupId;
     private String name;
     private String comments;
@@ -52,6 +53,15 @@ public class RemoteProcessGroupPortDTO {
         this.comments = comments;
     }
 
+    @ApiModelProperty("The ID of the corresponding component that is under version control")
+    public String getVersionedComponentId() {
+        return versionedComponentId;
+    }
+
+    public void setVersionedComponentId(final String id) {
+        this.versionedComponentId = id;
+    }
+
     /**
      * @return number tasks that may transmit flow files to the target port concurrently
      */

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
new file mode 100644
index 0000000..d27e830
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionControlInformationDTO.java
@@ -0,0 +1,98 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "versionControlInformation")
+public class VersionControlInformationDTO {
+    private String groupId;
+    private String registryId;
+    private String bucketId;
+    private String flowId;
+    private Integer version;
+    private Boolean modified;
+    private Boolean current;
+
+    @ApiModelProperty("The ID of the Process Group that is under version control")
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    @ApiModelProperty("The ID of the registry that the flow is stored in")
+    public String getRegistryId() {
+        return registryId;
+    }
+
+    public void setRegistryId(final String registryId) {
+        this.registryId = registryId;
+    }
+
+    @ApiModelProperty("The ID of the bucket that the flow is stored in")
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(final String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @ApiModelProperty("The ID of the flow")
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(final String flowId) {
+        this.flowId = flowId;
+    }
+
+    @ApiModelProperty("The version of the flow")
+    public Integer getVersion() {
+        return version;
+    }
+
+    public void setVersion(final Integer version) {
+        this.version = version;
+    }
+
+    @ApiModelProperty(readOnly=true,
+        value = "Whether or not the flow has been modified since it was last synced to the Flow Registry. The value will be null if this information is not yet known.")
+    public Boolean getModified() {
+        return modified;
+    }
+
+    public void setModified(Boolean modified) {
+        this.modified = modified;
+    }
+
+    @ApiModelProperty(readOnly=true,
+        value = "Whether or not this is the most recent version of the flow in the Flow Registry. The value will be null if this information is not yet known.")
+    public Boolean getCurrent() {
+        return current;
+    }
+
+    public void setCurrent(Boolean current) {
+        this.current = current;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
new file mode 100644
index 0000000..27a83e6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowDTO.java
@@ -0,0 +1,76 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "versionedFlow")
+public class VersionedFlowDTO {
+    private String registryId = "default"; // placeholder for now.
+    private String bucketId;
+    private String flowId;
+    private String flowName;
+    private String description;
+
+    @ApiModelProperty("The ID of the registry that the flow is tracked to")
+    public String getRegistryId() {
+        return registryId;
+    }
+
+    public void setRegistryId(String registryId) {
+        this.registryId = registryId;
+    }
+
+    @ApiModelProperty("The ID of the bucket where the flow is stored")
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @ApiModelProperty(value = "The ID of the flow")
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    @ApiModelProperty("The name of the flow")
+    public String getFlowName() {
+        return flowName;
+    }
+
+    public void setFlowName(String flowName) {
+        this.flowName = flowName;
+    }
+
+    @ApiModelProperty("A description of the flow")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
new file mode 100644
index 0000000..aa42bf6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VersionedFlowUpdateRequestDTO.java
@@ -0,0 +1,90 @@
+/*
+ * 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.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.util.TimestampAdapter;
+
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.util.Date;
+
+@XmlType(name = "versionedFlowUpdateRequest")
+public class VersionedFlowUpdateRequestDTO {
+    private String requestId;
+    private String processGroupId;
+    private String uri;
+    private Date lastUpdated;
+    private boolean complete = false;
+    private String failureReason;
+
+    @ApiModelProperty("The unique ID of the Process Group that the variable registry belongs to")
+    public String getProcessGroupId() {
+        return processGroupId;
+    }
+
+    public void setProcessGroupId(String processGroupId) {
+        this.processGroupId = processGroupId;
+    }
+
+    @ApiModelProperty(value = "The unique ID of this request.", readOnly = true)
+    public String getRequestId() {
+        return requestId;
+    }
+
+    public void setRequestId(String requestId) {
+        this.requestId = requestId;
+    }
+
+    @ApiModelProperty(value = "The URI for future requests to this drop request.", readOnly = true)
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+
+    @XmlJavaTypeAdapter(TimestampAdapter.class)
+    @ApiModelProperty(value = "The last time this request was updated.", dataType = "string", readOnly = true)
+    public Date getLastUpdated() {
+        return lastUpdated;
+    }
+
+    public void setLastUpdated(Date lastUpdated) {
+        this.lastUpdated = lastUpdated;
+    }
+
+    @ApiModelProperty(value = "Whether or not this request has completed", readOnly = true)
+    public boolean isComplete() {
+        return complete;
+    }
+
+    public void setComplete(boolean complete) {
+        this.complete = complete;
+    }
+
+    @ApiModelProperty(value = "An explanation of why this request failed, or null if this request has not failed", readOnly = true)
+    public String getFailureReason() {
+        return failureReason;
+    }
+
+    public void setFailureReason(String reason) {
+        this.failureReason = reason;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
index 0f28f73..e0d8496 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.nifi.web.api.entity;
 
 import org.apache.nifi.web.api.dto.AffectedComponentDTO;
@@ -33,12 +32,13 @@ public class AffectedComponentEntity extends ComponentEntity implements Permissi
     /**
      * @return variable referencing components that is being serialized
      */
+    @Override
     public AffectedComponentDTO getComponent() {
         return component;
     }
 
+    @Override
     public void setComponent(AffectedComponentDTO component) {
         this.component = component;
     }
-
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlComponentMappingEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlComponentMappingEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlComponentMappingEntity.java
new file mode 100644
index 0000000..e1bd6b5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlComponentMappingEntity.java
@@ -0,0 +1,59 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Map;
+
+@XmlRootElement(name = "versionControlComponentMappingEntity")
+public class VersionControlComponentMappingEntity extends Entity {
+    private VersionControlInformationDTO versionControlDto;
+    private Map<String, String> versionControlComponentMapping;
+    private RevisionDTO processGroupRevision;
+
+    @ApiModelProperty("The Version Control information")
+    public VersionControlInformationDTO getVersionControlInformation() {
+        return versionControlDto;
+    }
+
+    public void setVersionControlInformation(VersionControlInformationDTO versionControlDto) {
+        this.versionControlDto = versionControlDto;
+    }
+
+    @ApiModelProperty("The mapping of Versioned Component Identifiers to instance ID's")
+    public Map<String, String> getVersionControlComponentMapping() {
+        return versionControlComponentMapping;
+    }
+
+    public void setVersionControlComponentMapping(Map<String, String> mapping) {
+        this.versionControlComponentMapping = mapping;
+    }
+
+    @ApiModelProperty("The revision of the Process Group")
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(RevisionDTO processGroupRevision) {
+        this.processGroupRevision = processGroupRevision;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
new file mode 100644
index 0000000..e8ec81f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionControlInformationEntity.java
@@ -0,0 +1,48 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "versionControlInformationEntity")
+public class VersionControlInformationEntity extends Entity {
+    private VersionControlInformationDTO versionControlDto;
+    private RevisionDTO processGroupRevision;
+
+    @ApiModelProperty("The Version Control information")
+    public VersionControlInformationDTO getVersionControlInformation() {
+        return versionControlDto;
+    }
+
+    public void setVersionControlInformation(VersionControlInformationDTO versionControlDto) {
+        this.versionControlDto = versionControlDto;
+    }
+
+    @ApiModelProperty("The Revision for the Process Group")
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(RevisionDTO revision) {
+        this.processGroupRevision = revision;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
new file mode 100644
index 0000000..b94255a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
@@ -0,0 +1,48 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.VersionedFlowDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "versionedFlow")
+public class VersionedFlowEntity extends Entity {
+    private VersionedFlowDTO versionedFlow;
+    private RevisionDTO processGroupRevision;
+
+    @ApiModelProperty("The versioned flow")
+    public VersionedFlowDTO getVersionedFlow() {
+        return versionedFlow;
+    }
+
+    public void setVersionedFlow(VersionedFlowDTO versionedFLow) {
+        this.versionedFlow = versionedFLow;
+    }
+
+    @ApiModelProperty("The Revision of the Process Group under Version Control")
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(final RevisionDTO revision) {
+        this.processGroupRevision = revision;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
new file mode 100644
index 0000000..170640d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
@@ -0,0 +1,58 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "versionedFlowSnapshot")
+public class VersionedFlowSnapshotEntity extends Entity {
+    private VersionedFlowSnapshot versionedFlowSnapshot;
+    private RevisionDTO processGroupRevision;
+    private String registryId;
+
+    @ApiModelProperty("The versioned flow snapshot")
+    public VersionedFlowSnapshot getVersionedFlowSnapshot() {
+        return versionedFlowSnapshot;
+    }
+
+    public void setVersionedFlow(VersionedFlowSnapshot versionedFlowSnapshot) {
+        this.versionedFlowSnapshot = versionedFlowSnapshot;
+    }
+
+    @ApiModelProperty("The Revision of the Process Group under Version Control")
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(final RevisionDTO revision) {
+        this.processGroupRevision = revision;
+    }
+
+    @ApiModelProperty("The ID of the Registry that this flow belongs to")
+    public String getRegistryId() {
+        return registryId;
+    }
+
+    public void setRegistryId(String registryId) {
+        this.registryId = registryId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowUpdateRequestEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowUpdateRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowUpdateRequestEntity.java
new file mode 100644
index 0000000..7211824
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowUpdateRequestEntity.java
@@ -0,0 +1,49 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.VersionedFlowUpdateRequestDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "versionedFlowUpdateRequestEntity")
+public class VersionedFlowUpdateRequestEntity extends Entity {
+    private VersionedFlowUpdateRequestDTO request;
+    private RevisionDTO processGroupRevision;
+
+    @ApiModelProperty("The revision for the Process Group that owns this variable registry.")
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(RevisionDTO revision) {
+        this.processGroupRevision = revision;
+    }
+
+    @ApiModelProperty("The Versioned Flow Update Request")
+    public VersionedFlowUpdateRequestDTO getRequest() {
+        return request;
+    }
+
+    public void setRequest(VersionedFlowUpdateRequestDTO request) {
+        this.request = request;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerEndpointMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerEndpointMerger.java
index 6e38860..804d59d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerEndpointMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/ControllerEndpointMerger.java
@@ -33,7 +33,6 @@ import java.util.regex.Pattern;
 
 public class ControllerEndpointMerger extends AbstractSingleDTOEndpoint<ControllerEntity, ControllerDTO> {
     public static final Pattern CONTROLLER_URI_PATTERN = Pattern.compile("/nifi-api/site-to-site");
-    private PortEntityMerger portMerger = new PortEntityMerger();
 
     @Override
     protected Class<ControllerEntity> getEntityClass() {
@@ -47,7 +46,6 @@ public class ControllerEndpointMerger extends AbstractSingleDTOEndpoint<Controll
 
     @Override
     protected void mergeResponses(ControllerDTO clientDto, Map<NodeIdentifier, ControllerDTO> dtoMap, Set<NodeResponse> successfulResponses, Set<NodeResponse> problematicResponses) {
-        ControllerDTO mergedController = clientDto;
         final Map<String, Map<NodeIdentifier, PortDTO>> inputPortMap = new HashMap<>(); // map of port id to map of node id to port dto
         final Map<String, Map<NodeIdentifier, PortDTO>> outputPortMap = new HashMap<>(); // map of port id to map of node id to port dto
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowConfigurationEndpointMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowConfigurationEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowConfigurationEndpointMerger.java
index 22fa684..6ba2859 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowConfigurationEndpointMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/FlowConfigurationEndpointMerger.java
@@ -47,7 +47,6 @@ public class FlowConfigurationEndpointMerger extends AbstractNodeStatusEndpoint<
     protected void mergeResponses(FlowConfigurationDTO clientDto, Map<NodeIdentifier, FlowConfigurationDTO> dtoMap, NodeIdentifier selectedNodeId) {
 
         for (final Map.Entry<NodeIdentifier, FlowConfigurationDTO> entry : dtoMap.entrySet()) {
-            final NodeIdentifier nodeId = entry.getKey();
             final FlowConfigurationDTO toMerge = entry.getValue();
             if (toMerge != clientDto) {
                 clientDto.setSupportsConfigurableAuthorizer(clientDto.getSupportsConfigurableAuthorizer() && toMerge.getSupportsConfigurableAuthorizer());

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VersionControlInformationEndpointMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VersionControlInformationEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VersionControlInformationEndpointMerger.java
new file mode 100644
index 0000000..14e3bf6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VersionControlInformationEndpointMerger.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cluster.coordination.http.endpoints;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.apache.nifi.cluster.coordination.http.EndpointResponseMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.manager.VersionControlInformationEntityMerger;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+
+public class VersionControlInformationEndpointMerger extends AbstractSingleEntityEndpoint<VersionControlInformationEntity> implements EndpointResponseMerger {
+    public static final Pattern VERSION_CONTROL_URI_PATTERN = Pattern.compile("/nifi-api/versions/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))");
+    private final VersionControlInformationEntityMerger versionControlInfoEntityMerger = new VersionControlInformationEntityMerger();
+
+    @Override
+    public boolean canHandle(final URI uri, final String method) {
+        if (("GET".equalsIgnoreCase(method) || "POST".equalsIgnoreCase(method)) && (VERSION_CONTROL_URI_PATTERN.matcher(uri.getPath()).matches())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected Class<VersionControlInformationEntity> getEntityClass() {
+        return VersionControlInformationEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(final VersionControlInformationEntity clientEntity, final Map<NodeIdentifier, VersionControlInformationEntity> entityMap,
+        final Set<NodeResponse> successfulResponses, final Set<NodeResponse> problematicResponses) {
+
+        versionControlInfoEntityMerger.merge(clientEntity, entityMap);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/RequestReplicator.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/RequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/RequestReplicator.java
index 1fd6a49..a7177d4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/RequestReplicator.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/RequestReplicator.java
@@ -21,6 +21,7 @@ import java.net.URI;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.cluster.protocol.NodeIdentifier;
 
 public interface RequestReplicator {
@@ -71,7 +72,6 @@ public interface RequestReplicator {
      */
     void shutdown();
 
-
     /**
      * Replicates a request to each node in the cluster. If the request attempts to modify the flow and there is a node
      * that is not currently connected, an Exception will be thrown. Otherwise, the returned AsyncClusterResponse object
@@ -89,24 +89,64 @@ public interface RequestReplicator {
     AsyncClusterResponse replicate(String method, URI uri, Object entity, Map<String, String> headers);
 
     /**
+     * Replicates a request to each node in the cluster. If the request attempts to modify the flow and there is a node
+     * that is not currently connected, an Exception will be thrown. Otherwise, the returned AsyncClusterResponse object
+     * will contain the results that are immediately available, as well as an identifier for obtaining an updated result
+     * later. NOTE: This method will ALWAYS indicate that the request has been replicated.
+     *
+     * @param user the user making the request
+     * @param method the HTTP method (e.g., POST, PUT)
+     * @param uri the base request URI (up to, but not including, the query string)
+     * @param entity an entity
+     * @param headers any HTTP headers
+     * @return an AsyncClusterResponse that indicates the current status of the request and provides an identifier for obtaining an updated response later
+     * @throws ConnectingNodeMutableRequestException if the request attempts to modify the flow and there is a node that is in the CONNECTING state
+     * @throws DisconnectedNodeMutableRequestException if the request attempts to modify the flow and there is a node that is in the DISCONNECTED state
+     */
+    AsyncClusterResponse replicate(NiFiUser user, String method, URI uri, Object entity, Map<String, String> headers);
+
+    /**
+     * Requests are sent to each node in the given set of Node Identifiers. The returned AsyncClusterResponse object will contain
+     * the results that are immediately available, as well as an identifier for obtaining an updated result later.
+     * <p>
+     * HTTP DELETE, GET, HEAD, and OPTIONS methods will throw an IllegalArgumentException if used.
+     *
+     * @param nodeIds the node identifiers
+     * @param user the user making the request
+     * @param method the HTTP method (e.g., POST, PUT)
+     * @param uri the base request URI (up to, but not including, the query string)
+     * @param entity an entity
+     * @param headers any HTTP headers
+     * @param indicateReplicated if <code>true</code>, will add a header indicating to the receiving nodes that the request
+     *            has already been replicated, so the receiving node will not replicate the request itself.
+     * @param performVerification if <code>true</code>, and the request is mutable, will verify that all nodes are connected before
+     *            making the request and that all nodes are able to perform the request before acutally attempting to perform the task.
+     *            If false, will perform no such verification
+     * @return an AsyncClusterResponse that indicates the current status of the request and provides an identifier for obtaining an updated response later
+     */
+    AsyncClusterResponse replicate(Set<NodeIdentifier> nodeIds, NiFiUser user, String method, URI uri, Object entity, Map<String, String> headers, boolean indicateReplicated,
+        boolean performVerification);
+
+    /**
      * Requests are sent to each node in the given set of Node Identifiers. The returned AsyncClusterResponse object will contain
      * the results that are immediately available, as well as an identifier for obtaining an updated result later.
      * <p>
      * HTTP DELETE, GET, HEAD, and OPTIONS methods will throw an IllegalArgumentException if used.
      *
-     * @param nodeIds             the node identifiers
-     * @param method              the HTTP method (e.g., POST, PUT)
-     * @param uri                 the base request URI (up to, but not including, the query string)
-     * @param entity              an entity
-     * @param headers             any HTTP headers
-     * @param indicateReplicated  if <code>true</code>, will add a header indicating to the receiving nodes that the request
-     *                            has already been replicated, so the receiving node will not replicate the request itself.
+     * @param nodeIds the node identifiers
+     * @param method the HTTP method (e.g., POST, PUT)
+     * @param uri the base request URI (up to, but not including, the query string)
+     * @param entity an entity
+     * @param headers any HTTP headers
+     * @param indicateReplicated if <code>true</code>, will add a header indicating to the receiving nodes that the request
+     *            has already been replicated, so the receiving node will not replicate the request itself.
      * @param performVerification if <code>true</code>, and the request is mutable, will verify that all nodes are connected before
-     *                            making the request and that all nodes are able to perform the request before acutally attempting to perform the task.
-     *                            If false, will perform no such verification
+     *            making the request and that all nodes are able to perform the request before acutally attempting to perform the task.
+     *            If false, will perform no such verification
      * @return an AsyncClusterResponse that indicates the current status of the request and provides an identifier for obtaining an updated response later
      */
-    AsyncClusterResponse replicate(Set<NodeIdentifier> nodeIds, String method, URI uri, Object entity, Map<String, String> headers, boolean indicateReplicated, boolean performVerification);
+    AsyncClusterResponse replicate(Set<NodeIdentifier> nodeIds, String method, URI uri, Object entity, Map<String, String> headers, boolean indicateReplicated,
+        boolean performVerification);
 
 
     /**
@@ -122,6 +162,19 @@ public interface RequestReplicator {
     AsyncClusterResponse forwardToCoordinator(NodeIdentifier coordinatorNodeId, String method, URI uri, Object entity, Map<String, String> headers);
 
     /**
+     * Forwards a request to the Cluster Coordinator so that it is able to replicate the request to all nodes in the cluster.
+     *
+     * @param coordinatorNodeId the node identifier of the Cluster Coordinator
+     * @param user the user making the request
+     * @param method the HTTP method (e.g., POST, PUT)
+     * @param uri the base request URI (up to, but not including, the query string)
+     * @param entity an entity
+     * @param headers any HTTP headers
+     * @return an AsyncClusterResponse that indicates the current status of the request and provides an identifier for obtaining an updated response later
+     */
+    AsyncClusterResponse forwardToCoordinator(NodeIdentifier coordinatorNodeId, NiFiUser user, String method, URI uri, Object entity, Map<String, String> headers);
+
+    /**
      * <p>
      * Returns an AsyncClusterResponse that provides the most up-to-date status of the request with the given identifier.
      * If the request is finished, meaning that all nodes in the cluster have reported back their status or have timed out,
@@ -132,7 +185,7 @@ public interface RequestReplicator {
      *
      * @param requestIdentifier the identifier of the request to obtain a response for
      * @return an AsyncClusterResponse that provides the most up-to-date status of the request with the given identifier, or <code>null</code> if
-     * no request exists with the given identifier
+     *         no request exists with the given identifier
      */
     AsyncClusterResponse getClusterResponse(String requestIdentifier);
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
index bd7729c..bd1e4b3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
@@ -195,8 +195,14 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
         maintenanceExecutor.shutdown();
     }
 
+
     @Override
     public AsyncClusterResponse replicate(String method, URI uri, Object entity, Map<String, String> headers) {
+        return replicate(NiFiUserUtils.getNiFiUser(), method, uri, entity, headers);
+    }
+
+    @Override
+    public AsyncClusterResponse replicate(NiFiUser user, String method, URI uri, Object entity, Map<String, String> headers) {
         final Map<NodeConnectionState, List<NodeIdentifier>> stateMap = clusterCoordinator.getConnectionStates();
         final boolean mutable = isMutableRequest(method, uri.getPath());
 
@@ -237,11 +243,10 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
 
         final Set<NodeIdentifier> nodeIdSet = new HashSet<>(nodeIds);
 
-        return replicate(nodeIdSet, method, uri, entity, headers, true, true);
+        return replicate(nodeIdSet, user, method, uri, entity, headers, true, true);
     }
 
-    void updateRequestHeaders(final Map<String, String> headers) {
-        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+    void updateRequestHeaders(final Map<String, String> headers, final NiFiUser user) {
         if (user == null) {
             throw new AccessDeniedException("Unknown user");
         }
@@ -279,6 +284,13 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
 
     @Override
     public AsyncClusterResponse replicate(Set<NodeIdentifier> nodeIds, String method, URI uri, Object entity, Map<String, String> headers,
+                final boolean indicateReplicated, final boolean performVerification) {
+
+        return replicate(nodeIds, NiFiUserUtils.getNiFiUser(), method, uri, entity, headers, indicateReplicated, performVerification);
+    }
+
+    @Override
+    public AsyncClusterResponse replicate(Set<NodeIdentifier> nodeIds, final NiFiUser user, String method, URI uri, Object entity, Map<String, String> headers,
                                           final boolean indicateReplicated, final boolean performVerification) {
         final Map<String, String> updatedHeaders = new HashMap<>(headers);
 
@@ -288,7 +300,7 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
         }
 
         // include the proxied entities header
-        updateRequestHeaders(updatedHeaders);
+        updateRequestHeaders(updatedHeaders, user);
 
         if (indicateReplicated) {
             // If we are replicating a request and indicating that it is replicated, then this means that we are
@@ -324,12 +336,19 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
         }
     }
 
+
     @Override
     public AsyncClusterResponse forwardToCoordinator(final NodeIdentifier coordinatorNodeId, final String method, final URI uri, final Object entity, final Map<String, String> headers) {
+        return forwardToCoordinator(coordinatorNodeId, NiFiUserUtils.getNiFiUser(), method, uri, entity, headers);
+    }
+
+    @Override
+    public AsyncClusterResponse forwardToCoordinator(final NodeIdentifier coordinatorNodeId, final NiFiUser user, final String method,
+                final URI uri, final Object entity, final Map<String, String> headers) {
         final Map<String, String> updatedHeaders = new HashMap<>(headers);
 
         // include the proxied entities header
-        updateRequestHeaders(updatedHeaders);
+        updateRequestHeaders(updatedHeaders, user);
 
         return replicate(Collections.singleton(coordinatorNodeId), method, uri, entity, updatedHeaders, false, null, false, false, null);
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
index 89ac179..7e3bc5d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ConnectionEntityMerger.java
@@ -28,7 +28,6 @@ public class ConnectionEntityMerger implements ComponentEntityMerger<ConnectionE
     public void merge(ConnectionEntity clientEntity, Map<NodeIdentifier, ConnectionEntity> entityMap) {
         ComponentEntityMerger.super.merge(clientEntity, entityMap);
         for (Map.Entry<NodeIdentifier, ConnectionEntity> entry : entityMap.entrySet()) {
-            final NodeIdentifier nodeId = entry.getKey();
             final ConnectionEntity entityStatus = entry.getValue();
             if (entityStatus != clientEntity) {
                 mergeStatus(clientEntity.getStatus(), clientEntity.getPermissions().getCanRead(), entry.getValue().getStatus(), entry.getValue().getPermissions().getCanRead(), entry.getKey());

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/PortEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/PortEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/PortEntityMerger.java
index 2929741..3df3c16 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/PortEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/PortEntityMerger.java
@@ -31,7 +31,6 @@ public class PortEntityMerger implements ComponentEntityMerger<PortEntity>, Comp
     public void merge(PortEntity clientEntity, Map<NodeIdentifier, PortEntity> entityMap) {
         ComponentEntityMerger.super.merge(clientEntity, entityMap);
         for (Map.Entry<NodeIdentifier, PortEntity> entry : entityMap.entrySet()) {
-            final NodeIdentifier nodeId = entry.getKey();
             final PortEntity entityStatus = entry.getValue();
             if (entityStatus != clientEntity) {
                 mergeStatus(clientEntity.getStatus(), clientEntity.getPermissions().getCanRead(), entry.getValue().getStatus(), entry.getValue().getPermissions().getCanRead(), entry.getKey());

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
index 67278a7..457e75b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessGroupEntityMerger.java
@@ -17,6 +17,8 @@
 package org.apache.nifi.cluster.manager;
 
 import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.ProcessGroupDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 
@@ -31,6 +33,7 @@ public class ProcessGroupEntityMerger implements ComponentEntityMerger<ProcessGr
             final ProcessGroupEntity entityStatus = entry.getValue();
             if (entityStatus != clientEntity) {
                 mergeStatus(clientEntity.getStatus(), clientEntity.getPermissions().getCanRead(), entry.getValue().getStatus(), entry.getValue().getPermissions().getCanRead(), entry.getKey());
+                mergeVersionControlInformation(clientEntity, entityStatus);
             }
         }
     }
@@ -41,4 +44,18 @@ public class ProcessGroupEntityMerger implements ComponentEntityMerger<ProcessGr
         StatusMerger.merge(clientStatus, clientStatusReadablePermission, status, statusReadablePermission, statusNodeIdentifier.getId(), statusNodeIdentifier.getApiAddress(),
                 statusNodeIdentifier.getApiPort());
     }
+
+    private void mergeVersionControlInformation(ProcessGroupEntity targetGroup, ProcessGroupEntity toMerge) {
+        final ProcessGroupDTO targetGroupDto = targetGroup.getComponent();
+        final ProcessGroupDTO toMergeGroupDto = toMerge.getComponent();
+
+        final VersionControlInformationDTO targetVersionControl = targetGroupDto.getVersionControlInformation();
+        final VersionControlInformationDTO toMergeVersionControl = toMergeGroupDto.getVersionControlInformation();
+
+        if (targetVersionControl == null) {
+            targetGroupDto.setVersionControlInformation(toMergeGroupDto.getVersionControlInformation());
+        } else if (toMergeVersionControl != null) {
+            targetVersionControl.setCurrent(Boolean.TRUE.equals(targetVersionControl.getCurrent()) && Boolean.TRUE.equals(toMergeVersionControl.getCurrent()));
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessorEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessorEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessorEntityMerger.java
index 5c419e9..dffac49 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessorEntityMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/ProcessorEntityMerger.java
@@ -32,7 +32,6 @@ public class ProcessorEntityMerger implements ComponentEntityMerger<ProcessorEnt
     public void merge(ProcessorEntity clientEntity, Map<NodeIdentifier, ProcessorEntity> entityMap) {
         ComponentEntityMerger.super.merge(clientEntity, entityMap);
         for (Map.Entry<NodeIdentifier, ProcessorEntity> entry : entityMap.entrySet()) {
-            final NodeIdentifier nodeId = entry.getKey();
             final ProcessorEntity entityStatus = entry.getValue();
             if (entityStatus != clientEntity) {
                 mergeStatus(clientEntity.getStatus(), clientEntity.getPermissions().getCanRead(), entry.getValue().getStatus(), entry.getValue().getPermissions().getCanRead(), entry.getKey());

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
new file mode 100644
index 0000000..8d102df
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/VersionControlInformationEntityMerger.java
@@ -0,0 +1,48 @@
+/*
+ * 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.cluster.manager;
+
+import java.util.Map;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+
+public class VersionControlInformationEntityMerger {
+
+    public void merge(final VersionControlInformationEntity clientEntity, final Map<NodeIdentifier, VersionControlInformationEntity> entityMap) {
+
+        final VersionControlInformationDTO clientDto = clientEntity.getVersionControlInformation();
+
+        // We need to merge the 'current' and 'modified' flags because these are updated by nodes in the background. Since
+        // the nodes can synchronize with the Flow Registry at different intervals, we have to determine how to handle these
+        // flags if different nodes report different values for them.
+        entityMap.values().stream()
+            .filter(entity -> entity != clientEntity)
+            .forEach(entity -> {
+                final VersionControlInformationDTO dto = entity.getVersionControlInformation();
+
+                // We consider the flow to be current only if ALL nodes indicate that it is current
+                clientDto.setCurrent(Boolean.TRUE.equals(clientDto.getCurrent()) && Boolean.TRUE.equals(dto.getCurrent()));
+
+                // We consider the flow to be modified if ANY node indicates that it is modified
+                clientDto.setModified(Boolean.TRUE.equals(clientDto.getModified()) || Boolean.TRUE.equals(dto.getModified()));
+            });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/FileBasedClusterNodeFirewallFactoryBean.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/FileBasedClusterNodeFirewallFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/FileBasedClusterNodeFirewallFactoryBean.java
index a86fc79..3e76de6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/FileBasedClusterNodeFirewallFactoryBean.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/spring/FileBasedClusterNodeFirewallFactoryBean.java
@@ -25,14 +25,14 @@ import org.springframework.beans.factory.FactoryBean;
 /**
  * Factory bean for creating a singleton FileBasedClusterNodeFirewall instance.
  */
-public class FileBasedClusterNodeFirewallFactoryBean implements FactoryBean {
+public class FileBasedClusterNodeFirewallFactoryBean implements FactoryBean<FileBasedClusterNodeFirewall> {
 
     private FileBasedClusterNodeFirewall firewall;
 
     private NiFiProperties properties;
 
     @Override
-    public Object getObject() throws Exception {
+    public FileBasedClusterNodeFirewall getObject() throws Exception {
         if (firewall == null) {
             final File config = properties.getClusterNodeFirewallFile();
             final File restoreDirectory = properties.getRestoreDirectory();
@@ -44,7 +44,7 @@ public class FileBasedClusterNodeFirewallFactoryBean implements FactoryBean {
     }
 
     @Override
-    public Class getObjectType() {
+    public Class<FileBasedClusterNodeFirewall> getObjectType() {
         return FileBasedClusterNodeFirewall.class;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java
index 836751c..1f0ceb5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestThreadPoolRequestReplicator.java
@@ -19,6 +19,7 @@ package org.apache.nifi.cluster.coordination.http.replication;
 import org.apache.commons.collections4.map.MultiValueMap;
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
 import org.apache.nifi.authorization.user.StandardNiFiUser;
 import org.apache.nifi.authorization.user.StandardNiFiUser.Builder;
 import org.apache.nifi.cluster.coordination.ClusterCoordinator;
@@ -443,7 +444,7 @@ public class TestThreadPoolRequestReplicator {
 
                 // ensure the proxied entities header is set
                 final Map<String, String> updatedHeaders = new HashMap<>();
-                replicator.updateRequestHeaders(updatedHeaders);
+                replicator.updateRequestHeaders(updatedHeaders, NiFiUserUtils.getNiFiUser());
 
                 // Pass in Collections.emptySet() for the node ID's so that an Exception is thrown
                 replicator.replicate(Collections.emptySet(), "GET", new URI("localhost:8080/nifi"), Collections.emptyMap(),
@@ -501,7 +502,7 @@ public class TestThreadPoolRequestReplicator {
 
             // ensure the proxied entities header is set
             final Map<String, String> updatedHeaders = new HashMap<>();
-            replicator.updateRequestHeaders(updatedHeaders);
+            replicator.updateRequestHeaders(updatedHeaders, NiFiUserUtils.getNiFiUser());
 
             replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, updatedHeaders, true, null, true, true, monitor);
 
@@ -554,7 +555,7 @@ public class TestThreadPoolRequestReplicator {
 
             // ensure the proxied entities header is set
             final Map<String, String> updatedHeaders = new HashMap<>();
-            replicator.updateRequestHeaders(updatedHeaders);
+            replicator.updateRequestHeaders(updatedHeaders, NiFiUserUtils.getNiFiUser());
 
             replicator.replicate(nodeIds, HttpMethod.GET, uri, entity, updatedHeaders, true, null, true, true, monitor);
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java
index cace715..44d4905 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Node.java
@@ -66,6 +66,7 @@ import org.apache.nifi.io.socket.SocketConfiguration;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.SystemBundle;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.reporting.Severity;
 import org.apache.nifi.util.NiFiProperties;
@@ -147,7 +148,7 @@ public class Node {
         final HeartbeatMonitor heartbeatMonitor = createHeartbeatMonitor();
         flowController = FlowController.createClusteredInstance(Mockito.mock(FlowFileEventRepository.class), nodeProperties,
             null, null, StringEncryptor.createEncryptor(nodeProperties), protocolSender, Mockito.mock(BulletinRepository.class), clusterCoordinator,
-            heartbeatMonitor, electionManager, VariableRegistry.EMPTY_REGISTRY);
+            heartbeatMonitor, electionManager, VariableRegistry.EMPTY_REGISTRY, Mockito.mock(FlowRegistryClient.class));
 
         try {
             flowController.initializeFlow();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
index 0cf5906..d1bce36 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml
@@ -58,5 +58,9 @@
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-framework-authorization</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-data-model</artifactId>
+        </dependency>
     </dependencies>
 </project>


[46/50] nifi git commit: NIFI-4436: - Addressing PR feedback. - Addressing two phase commit logic issue when changing the flow version.

Posted by bb...@apache.org.
NIFI-4436:
- Addressing PR feedback.
- Addressing two phase commit logic issue when changing the flow version.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/b3e1584e
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/b3e1584e
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/b3e1584e

Branch: refs/heads/master
Commit: b3e1584ef4f5b7de8cf8517a8b950125f82832cb
Parents: 63544c8
Author: Matt Gilman <ma...@gmail.com>
Authored: Wed Jan 3 17:16:57 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:57 2018 -0500

----------------------------------------------------------------------
 .../apache/nifi/web/api/VersionsResource.java   | 154 +++++++++++++------
 .../main/webapp/js/nf/canvas/nf-flow-version.js |  27 +++-
 2 files changed, 134 insertions(+), 47 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/b3e1584e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index af9a515..53dc091 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -58,6 +58,7 @@ import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 import org.apache.nifi.web.api.dto.VersionedFlowUpdateRequestDTO;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.CreateActiveRequestEntity;
+import org.apache.nifi.web.api.entity.Entity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
@@ -724,19 +725,19 @@ public class VersionsResource extends ApplicationResource {
             throw new IllegalArgumentException("Process Group Revision must be specified.");
         }
 
-        final VersionedFlowSnapshot flowSnapshot = requestEntity.getVersionedFlowSnapshot();
-        if (flowSnapshot == null) {
+        final VersionedFlowSnapshot requestFlowSnapshot = requestEntity.getVersionedFlowSnapshot();
+        if (requestFlowSnapshot == null) {
             throw new IllegalArgumentException("Versioned Flow Snapshot must be supplied.");
         }
 
-        final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata();
-        if (snapshotMetadata == null) {
+        final VersionedFlowSnapshotMetadata requestSnapshotMetadata = requestFlowSnapshot.getSnapshotMetadata();
+        if (requestSnapshotMetadata == null) {
             throw new IllegalArgumentException("Snapshot Metadata must be supplied.");
         }
-        if (snapshotMetadata.getBucketIdentifier() == null) {
+        if (requestSnapshotMetadata.getBucketIdentifier() == null) {
             throw new IllegalArgumentException("The Bucket ID must be supplied.");
         }
-        if (snapshotMetadata.getFlowIdentifier() == null) {
+        if (requestSnapshotMetadata.getFlowIdentifier() == null) {
             throw new IllegalArgumentException("The Flow ID must be supplied.");
         }
 
@@ -761,9 +762,12 @@ public class VersionsResource extends ApplicationResource {
                 // the client has explicitly indicated the dataflow that the Process Group should
                 // provide and provided the Revision to ensure that they have the most up-to-date
                 // view of the Process Group.
-                serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, false);
+                serviceFacade.verifyCanUpdate(groupId, requestFlowSnapshot, true, false);
             },
             (rev, entity) -> {
+                final VersionedFlowSnapshot flowSnapshot = entity.getVersionedFlowSnapshot();
+                final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata();
+
                 final Bucket bucket = flowSnapshot.getBucket();
                 final VersionedFlow flow = flowSnapshot.getFlow();
 
@@ -1000,7 +1004,7 @@ public class VersionsResource extends ApplicationResource {
     })
     public Response initiateVersionControlUpdate(
         @ApiParam("The process group id.") @PathParam("id") final String groupId,
-        @ApiParam(value = "The controller service configuration details.", required = true) final VersionControlInformationEntity requestEntity) throws IOException {
+        @ApiParam(value = "The controller service configuration details.", required = true) final VersionControlInformationEntity requestEntity) {
 
         // Verify the request
         final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
@@ -1008,26 +1012,26 @@ public class VersionsResource extends ApplicationResource {
             throw new IllegalArgumentException("Process Group Revision must be specified");
         }
 
-        final VersionControlInformationDTO versionControlInfoDto = requestEntity.getVersionControlInformation();
-        if (versionControlInfoDto == null) {
+        final VersionControlInformationDTO requestVersionControlInfoDto = requestEntity.getVersionControlInformation();
+        if (requestVersionControlInfoDto == null) {
             throw new IllegalArgumentException("Version Control Information must be supplied.");
         }
-        if (versionControlInfoDto.getGroupId() == null) {
+        if (requestVersionControlInfoDto.getGroupId() == null) {
             throw new IllegalArgumentException("The Process Group ID must be supplied.");
         }
-        if (!versionControlInfoDto.getGroupId().equals(groupId)) {
+        if (!requestVersionControlInfoDto.getGroupId().equals(groupId)) {
             throw new IllegalArgumentException("The Process Group ID in the request body does not match the Process Group ID of the requested resource.");
         }
-        if (versionControlInfoDto.getBucketId() == null) {
+        if (requestVersionControlInfoDto.getBucketId() == null) {
             throw new IllegalArgumentException("The Bucket ID must be supplied.");
         }
-        if (versionControlInfoDto.getFlowId() == null) {
+        if (requestVersionControlInfoDto.getFlowId() == null) {
             throw new IllegalArgumentException("The Flow ID must be supplied.");
         }
-        if (versionControlInfoDto.getRegistryId() == null) {
+        if (requestVersionControlInfoDto.getRegistryId() == null) {
             throw new IllegalArgumentException("The Registry ID must be supplied.");
         }
-        if (versionControlInfoDto.getVersion() == null) {
+        if (requestVersionControlInfoDto.getVersion() == null) {
             throw new IllegalArgumentException("The Version of the flow must be supplied.");
         }
 
@@ -1037,7 +1041,6 @@ public class VersionsResource extends ApplicationResource {
         final boolean replicateRequest = isReplicateRequest();
         final ComponentLifecycle componentLifecycle = replicateRequest ? clusterComponentLifecycle : localComponentLifecycle;
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        final String idGenerationSeed = getIdGenerationSeed().orElse(null);
 
 
         // Workflow for this process:
@@ -1082,11 +1085,14 @@ public class VersionsResource extends ApplicationResource {
         // Step 1: Determine which components will be affected by updating the version
         final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, user);
 
-        final URI exampleUri = getAbsolutePath();
+        // build a request wrapper
+        final InitiateChangeFlowVersionRequestWrapper requestWrapper = new InitiateChangeFlowVersionRequestWrapper(requestEntity, componentLifecycle, getAbsolutePath(), affectedComponents,
+                replicateRequest, flowSnapshot);
+
         final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
         return withWriteLock(
             serviceFacade,
-            requestEntity,
+            requestWrapper,
             requestRevision,
             lookup -> {
                 // Step 2: Verify READ and WRITE permissions for user, for every component.
@@ -1106,7 +1112,9 @@ public class VersionsResource extends ApplicationResource {
                 // Step 5: Verify that Process Group is not 'dirty'
                 serviceFacade.verifyCanUpdate(groupId, flowSnapshot, false, true);
             },
-            (revision, processGroupEntity) -> {
+            (revision, wrapper) -> {
+                final String idGenerationSeed = getIdGenerationSeed().orElse(null);
+
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
@@ -1115,8 +1123,9 @@ public class VersionsResource extends ApplicationResource {
                 // Submit the request to be performed in the background
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
-                        final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, processGroupEntity, flowSnapshot, request, idGenerationSeed, true, true);
+                        final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, wrapper.getComponentLifecycle(), wrapper.getExampleUri(),
+                            wrapper.getAffectedComponents(), user, wrapper.isReplicateRequest(), revision, wrapper.getVersionControlInformationEntity(), wrapper.getFlowSnapshot(), request,
+                            idGenerationSeed, true, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final Exception e) {
@@ -1139,7 +1148,7 @@ public class VersionsResource extends ApplicationResource {
                 updateRequestDto.setState(request.getState());
 
                 final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
-                final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revision);
+                final RevisionDTO groupRevision = serviceFacade.getProcessGroup(groupId).getRevision();
                 updateRequestEntity.setProcessGroupRevision(groupRevision);
                 updateRequestEntity.setRequest(updateRequestDto);
 
@@ -1187,26 +1196,26 @@ public class VersionsResource extends ApplicationResource {
             throw new IllegalArgumentException("Process Group Revision must be specified");
         }
 
-        final VersionControlInformationDTO versionControlInfoDto = requestEntity.getVersionControlInformation();
-        if (versionControlInfoDto == null) {
+        final VersionControlInformationDTO requestVersionControlInfoDto = requestEntity.getVersionControlInformation();
+        if (requestVersionControlInfoDto == null) {
             throw new IllegalArgumentException("Version Control Information must be supplied.");
         }
-        if (versionControlInfoDto.getGroupId() == null) {
+        if (requestVersionControlInfoDto.getGroupId() == null) {
             throw new IllegalArgumentException("The Process Group ID must be supplied.");
         }
-        if (!versionControlInfoDto.getGroupId().equals(groupId)) {
+        if (!requestVersionControlInfoDto.getGroupId().equals(groupId)) {
             throw new IllegalArgumentException("The Process Group ID in the request body does not match the Process Group ID of the requested resource.");
         }
-        if (versionControlInfoDto.getBucketId() == null) {
+        if (requestVersionControlInfoDto.getBucketId() == null) {
             throw new IllegalArgumentException("The Bucket ID must be supplied.");
         }
-        if (versionControlInfoDto.getFlowId() == null) {
+        if (requestVersionControlInfoDto.getFlowId() == null) {
             throw new IllegalArgumentException("The Flow ID must be supplied.");
         }
-        if (versionControlInfoDto.getRegistryId() == null) {
+        if (requestVersionControlInfoDto.getRegistryId() == null) {
             throw new IllegalArgumentException("The Registry ID must be supplied.");
         }
-        if (versionControlInfoDto.getVersion() == null) {
+        if (requestVersionControlInfoDto.getVersion() == null) {
             throw new IllegalArgumentException("The Version of the flow must be supplied.");
         }
 
@@ -1216,7 +1225,6 @@ public class VersionsResource extends ApplicationResource {
         final boolean replicateRequest = isReplicateRequest();
         final ComponentLifecycle componentLifecycle = replicateRequest ? clusterComponentLifecycle : localComponentLifecycle;
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        final String idGenerationSeed = getIdGenerationSeed().orElse(null);
 
         // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
         final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation(), true);
@@ -1228,11 +1236,14 @@ public class VersionsResource extends ApplicationResource {
         // Step 1: Determine which components will be affected by updating the version
         final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, user);
 
-        final URI exampleUri = getAbsolutePath();
+        // build a request wrapper
+        final InitiateChangeFlowVersionRequestWrapper requestWrapper = new InitiateChangeFlowVersionRequestWrapper(requestEntity, componentLifecycle, getAbsolutePath(), affectedComponents,
+                replicateRequest, flowSnapshot);
+
         final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
         return withWriteLock(
             serviceFacade,
-            requestEntity,
+            requestWrapper,
             requestRevision,
             lookup -> {
                 // Step 2: Verify READ and WRITE permissions for user, for every component.
@@ -1251,7 +1262,10 @@ public class VersionsResource extends ApplicationResource {
                 // Step 4: Verify that Process Group is already under version control. If not, must start Version Control instead of updating flow
                 serviceFacade.verifyCanRevertLocalModifications(groupId, flowSnapshot);
             },
-            (revision, processGroupEntity) -> {
+            (revision, wrapper) -> {
+                final VersionControlInformationEntity versionControlInformationEntity = wrapper.getVersionControlInformationEntity();
+                final VersionControlInformationDTO versionControlInformationDTO = versionControlInformationEntity.getVersionControlInformation();
+
                 // Ensure that the information passed in is correct
                 final VersionControlInformationEntity currentVersionEntity = serviceFacade.getVersionControlInformation(groupId);
                 if (currentVersionEntity == null) {
@@ -1259,19 +1273,21 @@ public class VersionsResource extends ApplicationResource {
                 }
 
                 final VersionControlInformationDTO currentVersion = currentVersionEntity.getVersionControlInformation();
-                if (!currentVersion.getBucketId().equals(versionControlInfoDto.getBucketId())) {
+                if (!currentVersion.getBucketId().equals(versionControlInformationDTO.getBucketId())) {
                     throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
                 }
-                if (!currentVersion.getFlowId().equals(versionControlInfoDto.getFlowId())) {
+                if (!currentVersion.getFlowId().equals(versionControlInformationDTO.getFlowId())) {
                     throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
                 }
-                if (!currentVersion.getRegistryId().equals(versionControlInfoDto.getRegistryId())) {
+                if (!currentVersion.getRegistryId().equals(versionControlInformationDTO.getRegistryId())) {
                     throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
                 }
-                if (!currentVersion.getVersion().equals(versionControlInfoDto.getVersion())) {
+                if (!currentVersion.getVersion().equals(versionControlInformationDTO.getVersion())) {
                     throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
                 }
 
+                final String idGenerationSeed = getIdGenerationSeed().orElse(null);
+
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
@@ -1280,8 +1296,9 @@ public class VersionsResource extends ApplicationResource {
                 // Submit the request to be performed in the background
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
-                        final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, processGroupEntity, flowSnapshot, request, idGenerationSeed, false, true);
+                        final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, wrapper.getComponentLifecycle(), wrapper.getExampleUri(),
+                            wrapper.getAffectedComponents(), user, wrapper.isReplicateRequest(), revision, versionControlInformationEntity, wrapper.getFlowSnapshot(), request,
+                            idGenerationSeed, false, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final Exception e) {
@@ -1303,8 +1320,9 @@ public class VersionsResource extends ApplicationResource {
                 updateRequestDto.setPercentCompleted(request.getPercentComplete());
                 updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
 
+
                 final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
-                final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revision);
+                final RevisionDTO groupRevision = serviceFacade.getProcessGroup(groupId).getRevision();
                 updateRequestEntity.setProcessGroupRevision(groupRevision);
                 updateRequestEntity.setRequest(updateRequestDto);
 
@@ -1313,7 +1331,7 @@ public class VersionsResource extends ApplicationResource {
     }
 
     private VersionControlInformationEntity updateFlowVersion(final String groupId, final ComponentLifecycle componentLifecycle, final URI exampleUri,
-        final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user, final boolean replicateRequest, final VersionControlInformationEntity requestEntity,
+        final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user, final boolean replicateRequest, final Revision revision, final VersionControlInformationEntity requestEntity,
         final VersionedFlowSnapshot flowSnapshot, final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest, final String idGenerationSeed,
         final boolean verifyNotModified, final boolean updateDescendantVersionedFlows) throws LifecycleManagementException {
 
@@ -1372,7 +1390,7 @@ public class VersionsResource extends ApplicationResource {
                 headers.put("content-type", MediaType.APPLICATION_JSON);
 
                 final VersionedFlowSnapshotEntity snapshotEntity = new VersionedFlowSnapshotEntity();
-                snapshotEntity.setProcessGroupRevision(requestEntity.getProcessGroupRevision());
+                snapshotEntity.setProcessGroupRevision(dtoFactory.createRevisionDTO(revision));
                 snapshotEntity.setRegistryId(requestEntity.getVersionControlInformation().getRegistryId());
                 snapshotEntity.setVersionedFlow(flowSnapshot);
                 snapshotEntity.setUpdateDescendantVersionedFlows(updateDescendantVersionedFlows);
@@ -1408,8 +1426,6 @@ public class VersionsResource extends ApplicationResource {
                 serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, verifyNotModified);
 
                 // Step 11-12. Update Process Group to the new flow and update variable registry with any Variables that were added or removed
-                final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
-                final Revision revision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
                 final VersionControlInformationDTO requestVci = requestEntity.getVersionControlInformation();
 
                 final Bucket bucket = flowSnapshot.getBucket();
@@ -1571,4 +1587,50 @@ public class VersionsResource extends ApplicationResource {
             return updatePerformed;
         }
     }
+
+
+    private static class InitiateChangeFlowVersionRequestWrapper extends Entity {
+        final VersionControlInformationEntity versionControlInformationEntity;
+        final ComponentLifecycle componentLifecycle;
+        final URI exampleUri;
+        final Set<AffectedComponentEntity> affectedComponents;
+        final boolean replicateRequest;
+        final VersionedFlowSnapshot flowSnapshot;
+
+        public InitiateChangeFlowVersionRequestWrapper(final VersionControlInformationEntity versionControlInformationEntity, final ComponentLifecycle componentLifecycle,
+                                                       final URI exampleUri, final Set<AffectedComponentEntity> affectedComponents, final boolean replicateRequest,
+                                                       final VersionedFlowSnapshot flowSnapshot) {
+
+            this.versionControlInformationEntity = versionControlInformationEntity;
+            this.componentLifecycle = componentLifecycle;
+            this.exampleUri = exampleUri;
+            this.affectedComponents = affectedComponents;
+            this.replicateRequest = replicateRequest;
+            this.flowSnapshot = flowSnapshot;
+        }
+
+        public VersionControlInformationEntity getVersionControlInformationEntity() {
+            return versionControlInformationEntity;
+        }
+
+        public ComponentLifecycle getComponentLifecycle() {
+            return componentLifecycle;
+        }
+
+        public URI getExampleUri() {
+            return exampleUri;
+        }
+
+        public Set<AffectedComponentEntity> getAffectedComponents() {
+            return affectedComponents;
+        }
+
+        public boolean isReplicateRequest() {
+            return replicateRequest;
+        }
+
+        public VersionedFlowSnapshot getFlowSnapshot() {
+            return flowSnapshot;
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/b3e1584e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index b676f5b..96c467a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -214,8 +214,14 @@
                 });
             }
 
+            // determine the max registry height
+            var windowHeight = $(window).height();
+            var registryOffset = $('#import-flow-version-registry-combo').offset();
+            var registryMaxHeight = windowHeight - registryOffset.top - 64;
+
             // load the registries
             registryCombo.combo({
+                maxHeight: registryMaxHeight,
                 options: registries,
                 select: function (selectedOption) {
                     selectRegistry(dialog, selectedOption, bucketCombo, flowCombo, selectBucket, bucketCheck)
@@ -290,8 +296,14 @@
                 }
             }
 
+            // determine the max bucket height
+            var windowHeight = $(window).height();
+            var bucketOffset = $('#import-flow-version-bucket-combo').offset();
+            var bucketMaxHeight = windowHeight - bucketOffset.top - 64;
+
             // load the buckets
             bucketCombo.combo('destroy').combo({
+                maxHeight: bucketMaxHeight,
                 options: buckets,
                 select: selectBucket
             });
@@ -890,8 +902,14 @@
                 });
             }
 
+            // determine the max flow height
+            var windowHeight = $(window).height();
+            var flowOffset = $('#import-flow-version-name-combo').offset();
+            var flowMaxHeight = windowHeight - flowOffset.top - 64;
+
             // load the buckets
             $('#import-flow-version-name-combo').combo('destroy').combo({
+                maxHeight: flowMaxHeight,
                 options: versionedFlows,
                 select: function (selectedFlow) {
                     if (nfCommon.isDefinedAndNotNull(selectedFlow.value)) {
@@ -1284,7 +1302,6 @@
         if (nfCanvasUtils.getGroupId() === processGroupId) {
             // if reverting/changing current PG... reload/refresh this group/canvas
 
-            // TODO consider implementing this differently
             $.ajax({
                 type: 'GET',
                 url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId),
@@ -1295,6 +1312,14 @@
 
                 // update the component visibility
                 nfGraph.updateVisibility();
+
+                // update the breadcrumbs
+                var breadcrumbsCtrl = nfNgBridge.injector.get('breadcrumbsCtrl');
+                breadcrumbsCtrl.resetBreadcrumbs();
+                breadcrumbsCtrl.generateBreadcrumbs(response.processGroupFlow.breadcrumb);
+
+                // inform Angular app values have changed
+                nfNgBridge.digest();
             }).fail(nfErrorHandler.handleAjaxError);
         } else {
             // if reverting selected PG... reload selected PG to update counts, etc


[32/50] nifi git commit: NIFI-4436: Removed isCurrent, isModified from VersionControlInformation and associated DTO. Bug fixes & code refactoring

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
index 941aae0..8228a77 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
@@ -448,7 +448,10 @@ public class StandardRemoteProcessGroupDAO extends ComponentDAO implements Remot
             }
         }
 
-        remoteProcessGroup.getProcessGroup().onComponentModified();
+        final ProcessGroup group = remoteProcessGroup.getProcessGroup();
+        if (group != null) {
+            group.onComponentModified();
+        }
         return remoteProcessGroup;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fe8b30bf/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 43843ef..ebab24e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -52,8 +52,6 @@
         <property name="entityFactory" ref="entityFactory"/>
         <property name="authorizer" ref="authorizer"/>
         <property name="bulletinRepository" ref="bulletinRepository"/>
-        <property name="properties" ref="nifiProperties"/>
-        <property name="flowRegistryClient" ref="flowRegistryClient" />
     </bean>
 
     <!-- snippet utils -->


[45/50] nifi git commit: NIFI-4436: - Minor tweak to when the max height for the options list is calculated per PR comments. - Resolving logic issue in two phase commit when updating variable registry. - Fixing variable visibility.

Posted by bb...@apache.org.
NIFI-4436:
- Minor tweak to when the max height for the options list is calculated per PR comments.
- Resolving logic issue in two phase commit when updating variable registry.
- Fixing variable visibility.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/fa996cd4
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/fa996cd4
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/fa996cd4

Branch: refs/heads/master
Commit: fa996cd418601b0f1abeb5a926ac6146a96b0c07
Parents: f702f80
Author: Matt Gilman <ma...@gmail.com>
Authored: Wed Jan 3 20:57:37 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:57 2018 -0500

----------------------------------------------------------------------
 .../apache/nifi/web/api/ProcessGroupResource.java |  2 +-
 .../org/apache/nifi/web/api/VersionsResource.java | 12 ++++++------
 .../main/webapp/js/jquery/combo/jquery.combo.js   |  4 ++++
 .../main/webapp/js/nf/canvas/nf-flow-version.js   | 18 ------------------
 4 files changed, 11 insertions(+), 25 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/fa996cd4/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 96e1e08..6e99a07 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -620,7 +620,7 @@ public class ProcessGroupResource extends ApplicationResource {
         final VariableRegistryDTO requestRegistryDto = requestVariableRegistryEntity.getVariableRegistry();
         if (!groupId.equals(requestRegistryDto.getProcessGroupId())) {
             throw new IllegalArgumentException(String.format("The process group id (%s) in the request body does "
-                    + "not equal the process group id of the requested resource (%s).", registryDto.getProcessGroupId(), groupId));
+                    + "not equal the process group id of the requested resource (%s).", requestRegistryDto.getProcessGroupId(), groupId));
         }
 
         if (isReplicateRequest()) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/fa996cd4/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 53dc091..b106948 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -1590,12 +1590,12 @@ public class VersionsResource extends ApplicationResource {
 
 
     private static class InitiateChangeFlowVersionRequestWrapper extends Entity {
-        final VersionControlInformationEntity versionControlInformationEntity;
-        final ComponentLifecycle componentLifecycle;
-        final URI exampleUri;
-        final Set<AffectedComponentEntity> affectedComponents;
-        final boolean replicateRequest;
-        final VersionedFlowSnapshot flowSnapshot;
+        private final VersionControlInformationEntity versionControlInformationEntity;
+        private final ComponentLifecycle componentLifecycle;
+        private final URI exampleUri;
+        private final Set<AffectedComponentEntity> affectedComponents;
+        private final boolean replicateRequest;
+        private final VersionedFlowSnapshot flowSnapshot;
 
         public InitiateChangeFlowVersionRequestWrapper(final VersionControlInformationEntity versionControlInformationEntity, final ComponentLifecycle componentLifecycle,
                                                        final URI exampleUri, final Set<AffectedComponentEntity> affectedComponents, final boolean replicateRequest,

http://git-wip-us.apache.org/repos/asf/nifi/blob/fa996cd4/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/combo/jquery.combo.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/combo/jquery.combo.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/combo/jquery.combo.js
index 009f206..07d1c5b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/combo/jquery.combo.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/combo/jquery.combo.js
@@ -176,6 +176,10 @@
                             if (maxHeight > 0) {
                                 comboOptions.css('max-height', maxHeight + 'px');
                             }
+                        } else {
+                            var windowHeight = $(window).height();
+                            maxHeight = windowHeight - (position.top + Math.round(combo.outerHeight())) - 32;
+                            comboOptions.css('max-height', maxHeight + 'px');
                         }
 
                         // create the list that will contain the options

http://git-wip-us.apache.org/repos/asf/nifi/blob/fa996cd4/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 96c467a..ebbfd25 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -214,14 +214,8 @@
                 });
             }
 
-            // determine the max registry height
-            var windowHeight = $(window).height();
-            var registryOffset = $('#import-flow-version-registry-combo').offset();
-            var registryMaxHeight = windowHeight - registryOffset.top - 64;
-
             // load the registries
             registryCombo.combo({
-                maxHeight: registryMaxHeight,
                 options: registries,
                 select: function (selectedOption) {
                     selectRegistry(dialog, selectedOption, bucketCombo, flowCombo, selectBucket, bucketCheck)
@@ -296,14 +290,8 @@
                 }
             }
 
-            // determine the max bucket height
-            var windowHeight = $(window).height();
-            var bucketOffset = $('#import-flow-version-bucket-combo').offset();
-            var bucketMaxHeight = windowHeight - bucketOffset.top - 64;
-
             // load the buckets
             bucketCombo.combo('destroy').combo({
-                maxHeight: bucketMaxHeight,
                 options: buckets,
                 select: selectBucket
             });
@@ -902,14 +890,8 @@
                 });
             }
 
-            // determine the max flow height
-            var windowHeight = $(window).height();
-            var flowOffset = $('#import-flow-version-name-combo').offset();
-            var flowMaxHeight = windowHeight - flowOffset.top - 64;
-
             // load the buckets
             $('#import-flow-version-name-combo').combo('destroy').combo({
-                maxHeight: flowMaxHeight,
                 options: versionedFlows,
                 select: function (selectedFlow) {
                     if (nfCommon.isDefinedAndNotNull(selectedFlow.value)) {


[17/50] nifi git commit: NIFI-4436: Bug fixes - Checkpoint before allowing multiple Process Groups with same Versioned Component ID and same parent - Ensure that if flow update is cancelled while processors are being stopped/services disabled that we sto

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 3684f04..f2a207e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,21 +17,6 @@
 
 package org.apache.nifi.web.api;
 
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.DefaultValue;
@@ -92,10 +77,24 @@ import org.apache.nifi.web.util.AffectedComponentUtils;
 import org.apache.nifi.web.util.CancellableTimedPause;
 import org.apache.nifi.web.util.ComponentLifecycle;
 import org.apache.nifi.web.util.LifecycleManagementException;
-import org.apache.nifi.web.util.Pause;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -454,51 +453,15 @@ public class VersionsResource extends ApplicationResource {
                 super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
             },
             () -> {
-                final VersionControlInformationEntity entity = serviceFacade.getVersionControlInformation(groupId);
-                if (entity != null) {
-                    final String flowId = requestEntity.getVersionedFlow().getFlowId();
-                    if (flowId != null && flowId.equals(entity.getVersionControlInformation().getFlowId())) {
-                        // Flow ID is the same. We want to publish the Process Group as the next version of the Flow.
-                        // In order to do this, we have to ensure that the Process Group is 'current'.
-                        final Boolean current = entity.getVersionControlInformation().getCurrent();
-                        if (current == null) {
-                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
-                                + " because it is not yet known whether or not this Process Group is the most recent version of the flow. "
-                                + "Please try the request again after the Process Group has been synchronized with the Flow Registry.");
-                        }
-
-                        if (current == Boolean.FALSE) {
-                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
-                                + " because the Process Group in the flow is not synchronized with the most recent version of the Flow in the Flow Registry. "
-                                + "In order to publish a new version of the Flow, the Process Group must first be in synch with the latest version in the Flow Registry.");
-                        }
-
-                        // Flow ID matches. We want to publish the Process Group as the next version of the Flow, so we must
-                        // ensure that all other parameters match as well.
-                        if (!requestEntity.getVersionedFlow().getBucketId().equals(entity.getVersionControlInformation().getBucketId())) {
-                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
-                                + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
-                        }
-
-                        if (!requestEntity.getVersionedFlow().getRegistryId().equals(entity.getVersionControlInformation().getRegistryId())) {
-                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
-                                + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
-                        }
-
-                    } else if (flowId != null) {
-                        // Flow ID is specified but different. This is not allowed, because Flow ID's are automatically generated,
-                        // and if the client is specifying an ID then it is either trying to assign the ID of the Flow or it is
-                        // attempting to save a new version of a different flow. Saving a new version of a different Flow is
-                        // not allowed because the Process Group must be in synch with the latest version of the flow before that
-                        // can be done.
-                        throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
-                            + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
-                    }
-                }
+                final VersionedFlowDTO versionedFlow = requestEntity.getVersionedFlow();
+                final String registryId = versionedFlow.getRegistryId();
+                final String bucketId = versionedFlow.getBucketId();
+                final String flowId = versionedFlow.getFlowId();
+                serviceFacade.verifyCanSaveToFlowRegistry(groupId, registryId, bucketId, flowId);
             },
             (rev, flowEntity) -> {
                 // Register the current flow with the Flow Registry.
-                final VersionControlComponentMappingEntity mappingEntity = serviceFacade.registerFlowWithFlowRegistry(groupId, requestEntity);
+                final VersionControlComponentMappingEntity mappingEntity = serviceFacade.registerFlowWithFlowRegistry(groupId, flowEntity);
 
                 // Update the Process Group's Version Control Information
                 final VersionControlInformationEntity responseEntity = serviceFacade.setVersionControlInformation(rev, groupId,
@@ -756,7 +719,8 @@ public class VersionsResource extends ApplicationResource {
                 versionControlInfoDto.setRegistryId(requestEntity.getRegistryId());
                 versionControlInfoDto.setRegistryName(serviceFacade.getFlowRegistryName(requestEntity.getRegistryId()));
 
-                final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroup(rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false);
+                final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroup(rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false,
+                    entity.getUpdateDescendantVersionedFlows());
                 final VersionControlInformationDTO updatedVci = updatedGroup.getComponent().getVersionControlInformation();
 
                 final VersionControlInformationEntity responseEntity = new VersionControlInformationEntity();
@@ -1039,7 +1003,7 @@ public class VersionsResource extends ApplicationResource {
         // 14. Re-Start all Processors, Funnels, Ports that are affected and not removed.
 
         // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
-        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation());
+        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation(), true);
 
         // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
         // the flow snapshot to contain compatible bundles.
@@ -1085,7 +1049,7 @@ public class VersionsResource extends ApplicationResource {
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
                         final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, true);
+                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, true, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final LifecycleManagementException e) {
@@ -1188,7 +1152,7 @@ public class VersionsResource extends ApplicationResource {
         final String idGenerationSeed = getIdGenerationSeed().orElse(null);
 
         // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
-        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation());
+        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation(), false);
 
         // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
         // the flow snapshot to contain compatible bundles.
@@ -1221,8 +1185,7 @@ public class VersionsResource extends ApplicationResource {
             () -> {
                 // Step 3: Verify that all components in the snapshot exist on all nodes
                 // Step 4: Verify that Process Group is already under version control. If not, must start Version Control instead of updating flow
-                // Step 5: Verify that Process Group is not 'dirty'
-                serviceFacade.verifyCanUpdate(groupId, flowSnapshot, false, false);
+                serviceFacade.verifyCanRevertLocalModifications(groupId, flowSnapshot);
             },
             (revision, processGroupEntity) -> {
                 // Ensure that the information passed in is correct
@@ -1254,7 +1217,7 @@ public class VersionsResource extends ApplicationResource {
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
                         final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false);
+                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false, false);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final LifecycleManagementException e) {
@@ -1288,7 +1251,7 @@ public class VersionsResource extends ApplicationResource {
     private VersionControlInformationEntity updateFlowVersion(final String groupId, final ComponentLifecycle componentLifecycle, final URI exampleUri,
         final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user, final boolean replicateRequest, final VersionControlInformationEntity requestEntity,
         final VersionedFlowSnapshot flowSnapshot, final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest, final String idGenerationSeed,
-        final boolean verifyNotModified) throws LifecycleManagementException {
+        final boolean verifyNotModified, final boolean updateDescendantVersionedFlows) throws LifecycleManagementException {
 
         // Steps 6-7: Determine which components must be stopped and stop them.
         final Set<String> stoppableReferenceTypes = new HashSet<>();
@@ -1302,7 +1265,8 @@ public class VersionsResource extends ApplicationResource {
             .collect(Collectors.toSet());
 
         logger.info("Stopping {} Processors", runningComponents.size());
-        final Pause stopComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        final CancellableTimedPause stopComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        asyncRequest.setCancelCallback(stopComponentsPause::cancel);
         componentLifecycle.scheduleComponents(exampleUri, user, groupId, runningComponents, ScheduledState.STOPPED, stopComponentsPause);
 
         if (asyncRequest.isCancelled()) {
@@ -1317,7 +1281,8 @@ public class VersionsResource extends ApplicationResource {
             .collect(Collectors.toSet());
 
         logger.info("Disabling {} Controller Services", enabledServices.size());
-        final Pause disableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        final CancellableTimedPause disableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        asyncRequest.setCancelCallback(disableServicesPause::cancel);
         componentLifecycle.activateControllerServices(exampleUri, user, groupId, enabledServices, ControllerServiceState.DISABLED, disableServicesPause);
 
         if (asyncRequest.isCancelled()) {
@@ -1328,96 +1293,113 @@ public class VersionsResource extends ApplicationResource {
         logger.info("Updating Process Group with ID {} to version {} of the Versioned Flow", groupId, flowSnapshot.getSnapshotMetadata().getVersion());
         // If replicating request, steps 10-12 are performed on each node individually, and this is accomplished
         // by replicating a PUT to /nifi-api/versions/process-groups/{groupId}
-        if (replicateRequest) {
+        try {
+            if (replicateRequest) {
 
-            final URI updateUri;
-            try {
-                updateUri = new URI(exampleUri.getScheme(), exampleUri.getUserInfo(), exampleUri.getHost(),
-                    exampleUri.getPort(), "/nifi-api/versions/process-groups/" + groupId, null, exampleUri.getFragment());
-            } catch (URISyntaxException e) {
-                throw new RuntimeException(e);
-            }
+                final URI updateUri;
+                try {
+                    updateUri = new URI(exampleUri.getScheme(), exampleUri.getUserInfo(), exampleUri.getHost(),
+                        exampleUri.getPort(), "/nifi-api/versions/process-groups/" + groupId, null, exampleUri.getFragment());
+                } catch (URISyntaxException e) {
+                    throw new RuntimeException(e);
+                }
 
-            final Map<String, String> headers = new HashMap<>();
-            headers.put("content-type", MediaType.APPLICATION_JSON);
+                final Map<String, String> headers = new HashMap<>();
+                headers.put("content-type", MediaType.APPLICATION_JSON);
 
-            final VersionedFlowSnapshotEntity snapshotEntity = new VersionedFlowSnapshotEntity();
-            snapshotEntity.setProcessGroupRevision(requestEntity.getProcessGroupRevision());
-            snapshotEntity.setRegistryId(requestEntity.getVersionControlInformation().getRegistryId());
-            snapshotEntity.setVersionedFlow(flowSnapshot);
+                final VersionedFlowSnapshotEntity snapshotEntity = new VersionedFlowSnapshotEntity();
+                snapshotEntity.setProcessGroupRevision(requestEntity.getProcessGroupRevision());
+                snapshotEntity.setRegistryId(requestEntity.getVersionControlInformation().getRegistryId());
+                snapshotEntity.setVersionedFlow(flowSnapshot);
+                snapshotEntity.setUpdateDescendantVersionedFlows(updateDescendantVersionedFlows);
 
-            final NodeResponse clusterResponse;
-            try {
-                if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                    clusterResponse = getRequestReplicator().replicate(user, HttpMethod.PUT, updateUri, snapshotEntity, headers).awaitMergedResponse();
-                } else {
-                    clusterResponse = getRequestReplicator().forwardToCoordinator(
-                        getClusterCoordinatorNode(), user, HttpMethod.PUT, updateUri, snapshotEntity, headers).awaitMergedResponse();
+                final NodeResponse clusterResponse;
+                try {
+                    logger.debug("Replicating PUT request to {} for user {}", updateUri, user);
+
+                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                        clusterResponse = getRequestReplicator().replicate(user, HttpMethod.PUT, updateUri, snapshotEntity, headers).awaitMergedResponse();
+                    } else {
+                        clusterResponse = getRequestReplicator().forwardToCoordinator(
+                            getClusterCoordinatorNode(), user, HttpMethod.PUT, updateUri, snapshotEntity, headers).awaitMergedResponse();
+                    }
+                } catch (final InterruptedException ie) {
+                    logger.warn("Interrupted while replicating PUT request to {} for user {}", updateUri, user);
+                    Thread.currentThread().interrupt();
+                    throw new LifecycleManagementException("Interrupted while updating flows across cluster", ie);
                 }
-            } catch (final InterruptedException ie) {
-                Thread.currentThread().interrupt();
-                throw new LifecycleManagementException("Interrupted while updating flows across cluster", ie);
-            }
 
-            final int disableServicesStatus = clusterResponse.getStatus();
-            if (disableServicesStatus != Status.OK.getStatusCode()) {
-                final String explanation = getResponseEntity(clusterResponse, String.class);
-                throw new LifecycleManagementException("Failed to update Flow on all nodes in cluster due to " + explanation);
+                final int updateFlowStatus = clusterResponse.getStatus();
+                if (updateFlowStatus != Status.OK.getStatusCode()) {
+                    final String explanation = getResponseEntity(clusterResponse, String.class);
+                    logger.error("Failed to update flow across cluster when replicating PUT request to {} for user {}. Received {} response with explanation: {}",
+                        updateUri, user, updateFlowStatus, explanation);
+                    throw new LifecycleManagementException("Failed to update Flow on all nodes in cluster due to " + explanation);
+                }
+
+            } else {
+                // Step 10: Ensure that if any connection exists in the flow and does not exist in the proposed snapshot,
+                // that it has no data in it. Ensure that no Input Port was removed, unless it currently has no incoming connections.
+                // Ensure that no Output Port was removed, unless it currently has no outgoing connections.
+                serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, verifyNotModified);
+
+                // Step 11-12. Update Process Group to the new flow and update variable registry with any Variables that were added or removed
+                final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+                final Revision revision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
+                final VersionControlInformationDTO requestVci = requestEntity.getVersionControlInformation();
+
+                final Bucket bucket = flowSnapshot.getBucket();
+                final VersionedFlow flow = flowSnapshot.getFlow();
+
+                final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
+                final VersionControlInformationDTO vci = new VersionControlInformationDTO();
+                vci.setBucketId(metadata.getBucketIdentifier());
+                vci.setBucketName(bucket.getName());
+                vci.setCurrent(flowSnapshot.isLatest());
+                vci.setFlowDescription(flow.getDescription());
+                vci.setFlowId(flow.getIdentifier());
+                vci.setFlowName(flow.getName());
+                vci.setGroupId(groupId);
+                vci.setModified(false);
+                vci.setRegistryId(requestVci.getRegistryId());
+                vci.setRegistryName(serviceFacade.getFlowRegistryName(requestVci.getRegistryId()));
+                vci.setVersion(metadata.getVersion());
+
+                serviceFacade.updateProcessGroupContents(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified, false, updateDescendantVersionedFlows);
             }
+        } finally {
+            if (!asyncRequest.isCancelled()) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Re-Enabling {} Controller Services: {}", enabledServices.size(), enabledServices);
+                }
 
-        } else {
-            // Step 10: Ensure that if any connection exists in the flow and does not exist in the proposed snapshot,
-            // that it has no data in it. Ensure that no Input Port was removed, unless it currently has no incoming connections.
-            // Ensure that no Output Port was removed, unless it currently has no outgoing connections.
-            serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, verifyNotModified);
-
-            // Step 11-12. Update Process Group to the new flow and update variable registry with any Variables that were added or removed
-            final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
-            final Revision revision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
-            final VersionControlInformationDTO requestVci = requestEntity.getVersionControlInformation();
-
-            final Bucket bucket = flowSnapshot.getBucket();
-            final VersionedFlow flow = flowSnapshot.getFlow();
-
-            final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
-            final VersionControlInformationDTO vci = new VersionControlInformationDTO();
-            vci.setBucketId(metadata.getBucketIdentifier());
-            vci.setBucketName(bucket.getName());
-            vci.setCurrent(flowSnapshot.isLatest());
-            vci.setFlowDescription(flow.getDescription());
-            vci.setFlowId(flow.getIdentifier());
-            vci.setFlowName(flow.getName());
-            vci.setGroupId(groupId);
-            vci.setModified(false);
-            vci.setRegistryId(requestVci.getRegistryId());
-            vci.setRegistryName(serviceFacade.getFlowRegistryName(requestVci.getRegistryId()));
-            vci.setVersion(metadata.getVersion());
-
-            serviceFacade.updateProcessGroupContents(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified, false);
-        }
+                asyncRequest.update(new Date(), "Re-Enabling Controller Services", 60);
 
-        if (asyncRequest.isCancelled()) {
-            return null;
-        }
-        asyncRequest.update(new Date(), "Re-Enabling Controller Services", 60);
+                // Step 13. Re-enable all disabled controller services
+                final CancellableTimedPause enableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+                asyncRequest.setCancelCallback(enableServicesPause::cancel);
+                final Set<AffectedComponentEntity> servicesToEnable = getUpdatedEntities(enabledServices, user);
+                logger.info("Successfully updated flow; re-enabling {} Controller Services", servicesToEnable.size());
+                componentLifecycle.activateControllerServices(exampleUri, user, groupId, servicesToEnable, ControllerServiceState.ENABLED, enableServicesPause);
+            }
 
-        // Step 13. Re-enable all disabled controller services
-        final Pause enableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
-        final Set<AffectedComponentEntity> servicesToEnable = getUpdatedEntities(enabledServices, user);
-        logger.info("Successfully updated flow; re-enabling {} Controller Services", servicesToEnable.size());
-        componentLifecycle.activateControllerServices(exampleUri, user, groupId, servicesToEnable, ControllerServiceState.ENABLED, enableServicesPause);
+            if (!asyncRequest.isCancelled()) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Restart {} Processors: {}", runningComponents.size(), runningComponents);
+                }
 
-        if (asyncRequest.isCancelled()) {
-            return null;
-        }
-        asyncRequest.update(new Date(), "Restarting Processors", 80);
+                asyncRequest.update(new Date(), "Restarting Processors", 80);
 
-        // Step 14. Restart all components
-        final Set<AffectedComponentEntity> componentsToStart = getUpdatedEntities(runningComponents, user);
-        final Pause startComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
-        logger.info("Restarting {} Processors", componentsToStart.size());
-        componentLifecycle.scheduleComponents(exampleUri, user, groupId, componentsToStart, ScheduledState.RUNNING, startComponentsPause);
+                // Step 14. Restart all components
+                final Set<AffectedComponentEntity> componentsToStart = getUpdatedEntities(runningComponents, user);
+                final CancellableTimedPause startComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+                asyncRequest.setCancelCallback(startComponentsPause::cancel);
+                logger.info("Restarting {} Processors", componentsToStart.size());
+                componentLifecycle.scheduleComponents(exampleUri, user, groupId, componentsToStart, ScheduledState.RUNNING, startComponentsPause);
+            }
+        }
 
+        asyncRequest.setCancelCallback(null);
         if (asyncRequest.isCancelled()) {
             return null;
         }
@@ -1426,6 +1408,7 @@ public class VersionsResource extends ApplicationResource {
         return serviceFacade.getVersionControlInformation(groupId);
     }
 
+
     /**
      * Extracts the response entity from the specified node response.
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
index 1309eee..3cecdeb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
@@ -99,4 +99,12 @@ public interface AsynchronousWebRequest<T> {
      * @return <code>true</code> if the request has been canceled, <code>false</code> otherwise
      */
     boolean isCancelled();
+
+    /**
+     * Sets the cancel callback to the given runnable, so that if {@link #cancel()} is called, the given {@link Runnable} will be triggered.
+     * If <code>null</code> is passed, no operation will be triggered when the task is cancelled.
+     *
+     * @param runnable the callback
+     */
+    void setCancelCallback(Runnable runnable);
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
index 4810a32..8e2e221 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
@@ -34,6 +34,7 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
     private volatile String failureReason;
     private volatile boolean cancelled;
     private volatile T results;
+    private volatile Runnable cancelCallback;
 
     public StandardAsynchronousWebRequest(final String requestId, final String processGroupId, final NiFiUser user, final String state) {
         this.id = requestId;
@@ -57,6 +58,11 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
     }
 
     @Override
+    public void setCancelCallback(final Runnable runnable) {
+        this.cancelCallback = runnable;
+    }
+
+    @Override
     public void markComplete(final T results) {
         this.complete = true;
         this.results = results;
@@ -130,6 +136,7 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
         percentComplete = 100;
         state = "Canceled by user";
         setFailureReason("Request cancelled by user");
+        cancelCallback.run();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 6077268..7d40473 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -118,6 +118,7 @@ import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.diff.FlowComparison;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedFunnel;
@@ -2202,15 +2203,23 @@ public final class DtoFactory {
 
     private ComponentDifferenceDTO createComponentDifference(final FlowDifference difference) {
         VersionedComponent component = difference.getComponentA();
-        if (component == null) {
+        if (component == null || difference.getComponentB() instanceof InstantiatedVersionedComponent) {
             component = difference.getComponentB();
         }
 
         final ComponentDifferenceDTO dto = new ComponentDifferenceDTO();
-        dto.setComponentId(component.getIdentifier());
         dto.setComponentName(component.getName());
         dto.setComponentType(component.getComponentType().name());
-        dto.setProcessGroupId(dto.getProcessGroupId());
+
+        if (component instanceof InstantiatedVersionedComponent) {
+            final InstantiatedVersionedComponent instantiatedComponent = (InstantiatedVersionedComponent) component;
+            dto.setComponentId(instantiatedComponent.getInstanceId());
+            dto.setProcessGroupId(instantiatedComponent.getInstanceGroupId());
+        } else {
+            dto.setComponentId(component.getIdentifier());
+            dto.setProcessGroupId(dto.getProcessGroupId());
+        }
+
         return dto;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index 7cf61ea..9259bf4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -114,10 +114,12 @@ public interface ProcessGroupDAO {
      * @param versionControlInformation the new Version Control Information
      * @param componentIdSeed the seed value to use for generating ID's for new components
      * @param updateSettings whether or not to update the process group's name and position
+     * @param updateDescendantVersionedFlows if a child/descendant Process Group is under Version Control, specifies whether or not to
+     *            update the contents of that Process Group
      * @return the process group
      */
     ProcessGroup updateProcessGroupFlow(String groupId, VersionedFlowSnapshot proposedSnapshot, VersionControlInformationDTO versionControlInformation, String componentIdSeed,
-        boolean verifyNotModified, boolean updateSettings);
+        boolean verifyNotModified, boolean updateSettings, boolean updateDescendantVersionedFlows);
 
     /**
      * Applies the given Version Control Information to the Process Group

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index e3c4725..bb7edb1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -29,6 +29,8 @@ import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
@@ -244,8 +246,12 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         final FlowRegistry flowRegistry = flowController.getFlowRegistryClient().getFlowRegistry(registryId);
         final String registryName = flowRegistry == null ? registryId : flowRegistry.getName();
 
+        final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
+        final VersionedProcessGroup flowSnapshot = mapper.mapProcessGroup(group, flowController.getFlowRegistryClient(), false);
+
         final StandardVersionControlInformation vci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
             .registryName(registryName)
+            .flowSnapshot(flowSnapshot)
             .modified(false)
             .current(true)
             .build();
@@ -264,9 +270,9 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
 
     @Override
     public ProcessGroup updateProcessGroupFlow(final String groupId, final VersionedFlowSnapshot proposedSnapshot, final VersionControlInformationDTO versionControlInformation,
-        final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings) {
+        final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
-        group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified, updateSettings);
+        group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows);
 
         final StandardVersionControlInformation svci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
             .flowSnapshot(proposedSnapshot.getFlowContents())

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
index 1f83a6f..dea43f6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
@@ -43,7 +43,7 @@ public class CancellableTimedPause implements Pause {
 
         long sysTime = System.nanoTime();
         final long maxWaitTime = System.nanoTime() + pauseNanos;
-        while (sysTime < maxWaitTime) {
+        while (sysTime < maxWaitTime && !cancelled) {
             try {
                 TimeUnit.NANOSECONDS.sleep(pauseNanos);
             } catch (final InterruptedException ie) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
index 4469ea1..11bd1b8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/SnippetUtils.java
@@ -421,18 +421,24 @@ public final class SnippetUtils {
         }
 
         // get a list of all names of process groups so that we can rename as needed.
-        final List<String> groupNames = new ArrayList<>();
+        final Set<String> groupNames = new HashSet<>();
         for (final ProcessGroup childGroup : group.getProcessGroups()) {
             groupNames.add(childGroup.getName());
         }
 
         if (snippetContents.getProcessGroups() != null) {
             for (final ProcessGroupDTO groupDTO : snippetContents.getProcessGroups()) {
-                String groupName = groupDTO.getName();
-                while (groupNames.contains(groupName)) {
-                    groupName = "Copy of " + groupName;
+                // If Version Control Information is present, then we don't want to rename the
+                // Process Group - we want it to remain the same as the one in Version Control.
+                // However, in order to disambiguate things, we generally do want to rename to
+                // 'Copy of...' so we do this only if there is no Version Control Information present.
+                if (groupDTO.getVersionControlInformation() == null) {
+                    String groupName = groupDTO.getName();
+                    while (groupNames.contains(groupName)) {
+                        groupName = "Copy of " + groupName;
+                    }
+                    groupDTO.setName(groupName);
                 }
-                groupDTO.setName(groupName);
                 groupNames.add(groupDTO.getName());
             }
         }


[05/50] nifi git commit: NIFI-4436: Added additional endpoints; bug fixes

Posted by bb...@apache.org.
NIFI-4436: Added additional endpoints; bug fixes

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/6aa8b5c6
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/6aa8b5c6
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/6aa8b5c6

Branch: refs/heads/master
Commit: 6aa8b5c61c734ce56aad7816b40df88d8316feeb
Parents: 7a0a900
Author: Mark Payne <ma...@hotmail.com>
Authored: Mon Oct 30 16:35:59 2017 -0400
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:52 2018 -0500

----------------------------------------------------------------------
 .../nifi/web/api/entity/ProcessGroupEntity.java |  17 +-
 .../org/apache/nifi/groups/ProcessGroup.java    |   3 +-
 .../apache/nifi/registry/flow/FlowRegistry.java |  30 ++
 .../nifi/registry/flow/FlowRegistryClient.java  |   6 +
 .../apache/nifi/controller/FlowController.java  | 103 ++++
 .../controller/StandardFlowSynchronizer.java    |  36 +-
 .../serialization/StandardFlowSerializer.java   |  29 +-
 .../nifi/fingerprint/FingerprintFactory.java    |  21 +
 .../nifi/groups/StandardProcessGroup.java       | 134 ++++--
 .../registry/flow/FileBasedFlowRegistry.java    | 478 +++++++++++++++++++
 .../flow/FileBasedFlowRegistryClient.java       | 435 -----------------
 .../flow/StandardFlowRegistryClient.java        |  75 +++
 .../flow/mapping/NiFiRegistryFlowMapper.java    |  95 +++-
 .../java/org/apache/nifi/util/BundleUtils.java  |  48 +-
 .../src/main/resources/FlowConfiguration.xsd    |  17 +
 .../src/main/resources/nifi-context.xml         |   4 +-
 .../service/mock/MockProcessGroup.java          |   2 +-
 .../fingerprint/FingerprintFactoryTest.java     |   8 +
 .../nifi/registry/flow/FlowRegistryUtils.java   |  74 +++
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  12 +-
 .../nifi/web/StandardNiFiServiceFacade.java     | 266 ++++++-----
 .../nifi/web/api/ProcessGroupResource.java      | 173 ++++---
 .../apache/nifi/web/api/VersionsResource.java   | 151 +++---
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  10 +
 .../nifi/web/controller/ControllerFacade.java   |   4 +
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |   3 +-
 .../org/apache/nifi/web/dao/RegistryDAO.java    |  35 ++
 .../nifi/web/dao/impl/FlowRegistryDAO.java      |  66 +++
 .../web/dao/impl/StandardProcessGroupDAO.java   |   5 +-
 .../src/main/resources/nifi-web-api-context.xml |   4 +
 30 files changed, 1578 insertions(+), 766 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
index 83af9d8..1e2a4b4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
@@ -16,11 +16,13 @@
  */
 package org.apache.nifi.web.api.entity;
 
-import io.swagger.annotations.ApiModelProperty;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
 
-import javax.xml.bind.annotation.XmlRootElement;
+import io.swagger.annotations.ApiModelProperty;
 
 /**
  * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a ProcessGroupDTO.
@@ -30,6 +32,7 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
 
     private ProcessGroupDTO component;
     private ProcessGroupStatusDTO status;
+    private VersionedFlowSnapshot versionedFlowSnapshot;
 
     private Integer runningCount;
     private Integer stoppedCount;
@@ -46,10 +49,12 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
      *
      * @return The ProcessGroupDTO object
      */
+    @Override
     public ProcessGroupDTO getComponent() {
         return component;
     }
 
+    @Override
     public void setComponent(ProcessGroupDTO component) {
         this.component = component;
     }
@@ -180,4 +185,12 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
         this.inactiveRemotePortCount = inactiveRemotePortCount;
     }
 
+    @ApiModelProperty(value = "Returns the Versioned Flow that describes the contents of the Versioned Flow to be imported", readOnly = true)
+    public VersionedFlowSnapshot getVersionedFlowSnapshot() {
+        return versionedFlowSnapshot;
+    }
+
+    public void setVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot) {
+        this.versionedFlowSnapshot = versionedFlowSnapshot;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index d335461..16b4b5e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -783,8 +783,9 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
      * @param verifyNotDirty whether or not to verify that the Process Group is not 'dirty'. If this value is <code>true</code>,
      *            and the Process Group has been modified since it was last synchronized with the Flow Registry, then this method will
      *            throw an IllegalStateException
+     * @param updateSettings whether or not to update the process group's name and positions
      */
-    void updateFlow(VersionedFlowSnapshot proposedSnapshot, String componentIdSeed, boolean verifyNotDirty);
+    void updateFlow(VersionedFlowSnapshot proposedSnapshot, String componentIdSeed, boolean verifyNotDirty, boolean updateSettings);
 
     /**
      * Verifies a template with the specified name can be created.

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
index 962a940..4efff94 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -24,6 +24,22 @@ import java.io.IOException;
 import java.util.Set;
 
 public interface FlowRegistry {
+    /**
+     * @return the ID of the Flow Registry
+     */
+    String getIdentifier();
+
+    /**
+     * @return the description of the Flow Registry
+     */
+    String getDescription();
+
+    /**
+     * Updates the Flow Registry's description
+     *
+     * @param description the description of the Flow Registry
+     */
+    void setDescription(String description);
 
     /**
      * @return the URL of the Flow Registry
@@ -31,11 +47,25 @@ public interface FlowRegistry {
     String getURL();
 
     /**
+     * Updates the Flow Registry's URL
+     *
+     * @param url the URL of the Flow Registry
+     */
+    void setURL(String url);
+
+    /**
      * @return the name of the Flow Registry
      */
     String getName();
 
     /**
+     * Updates the name of the Flow Registry
+     *
+     * @param name the name of the Flow Registry
+     */
+    void setName(String name);
+
+    /**
      * Gets the buckets for the specified user.
      *
      * @param user current user

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
index 83f66dc..77c2761 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryClient.java
@@ -34,4 +34,10 @@ public interface FlowRegistryClient {
     }
 
     Set<String> getRegistryIdentifiers();
+
+    void addFlowRegistry(FlowRegistry registry);
+
+    FlowRegistry addFlowRegistry(String registryId, String registryName, String registryUrl, String description);
+
+    FlowRegistry removeFlowRegistry(String registryId);
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 242ef6a..5ed5b6e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -165,6 +165,8 @@ import org.apache.nifi.provenance.StandardProvenanceEventRecord;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.VersionedConnection;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.registry.variable.StandardComponentVariableRegistry;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
@@ -2128,6 +2130,13 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         }
     }
 
+    private void verifyBundleInVersionedFlow(final org.apache.nifi.registry.flow.Bundle requiredBundle, final Set<BundleCoordinate> supportedBundles) {
+        final BundleCoordinate requiredCoordinate = new BundleCoordinate(requiredBundle.getGroup(), requiredBundle.getArtifact(), requiredBundle.getVersion());
+        if (!supportedBundles.contains(requiredCoordinate)) {
+            throw new IllegalStateException("Unsupported bundle: " + requiredCoordinate);
+        }
+    }
+
     private void verifyProcessorsInSnippet(final FlowSnippetDTO templateContents, final Map<String, Set<BundleCoordinate>> supportedTypes) {
         if (templateContents.getProcessors() != null) {
             templateContents.getProcessors().forEach(processor -> {
@@ -2150,6 +2159,28 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         }
     }
 
+    private void verifyProcessorsInVersionedFlow(final VersionedProcessGroup versionedFlow, final Map<String, Set<BundleCoordinate>> supportedTypes) {
+        if (versionedFlow.getProcessors() != null) {
+            versionedFlow.getProcessors().forEach(processor -> {
+                if (processor.getBundle() == null) {
+                    throw new IllegalArgumentException("Processor bundle must be specified.");
+                }
+
+                if (supportedTypes.containsKey(processor.getType())) {
+                    verifyBundleInVersionedFlow(processor.getBundle(), supportedTypes.get(processor.getType()));
+                } else {
+                    throw new IllegalStateException("Invalid Processor Type: " + processor.getType());
+                }
+            });
+        }
+
+        if (versionedFlow.getProcessGroups() != null) {
+            versionedFlow.getProcessGroups().forEach(processGroup -> {
+                verifyProcessorsInVersionedFlow(processGroup, supportedTypes);
+            });
+        }
+    }
+
     private void verifyControllerServicesInSnippet(final FlowSnippetDTO templateContents, final Map<String, Set<BundleCoordinate>> supportedTypes) {
         if (templateContents.getControllerServices() != null) {
             templateContents.getControllerServices().forEach(controllerService -> {
@@ -2172,6 +2203,28 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         }
     }
 
+    private void verifyControllerServicesInVersionedFlow(final VersionedProcessGroup versionedFlow, final Map<String, Set<BundleCoordinate>> supportedTypes) {
+        if (versionedFlow.getControllerServices() != null) {
+            versionedFlow.getControllerServices().forEach(controllerService -> {
+                if (supportedTypes.containsKey(controllerService.getType())) {
+                    if (controllerService.getBundle() == null) {
+                        throw new IllegalArgumentException("Controller Service bundle must be specified.");
+                    }
+
+                    verifyBundleInVersionedFlow(controllerService.getBundle(), supportedTypes.get(controllerService.getType()));
+                } else {
+                    throw new IllegalStateException("Invalid Controller Service Type: " + controllerService.getType());
+                }
+            });
+        }
+
+        if (versionedFlow.getProcessGroups() != null) {
+            versionedFlow.getProcessGroups().forEach(processGroup -> {
+                verifyControllerServicesInVersionedFlow(processGroup, supportedTypes);
+            });
+        }
+    }
+
     public void verifyComponentTypesInSnippet(final FlowSnippetDTO templateContents) {
         final Map<String, Set<BundleCoordinate>> processorClasses = new HashMap<>();
         for (final Class<?> c : ExtensionManager.getExtensions(Processor.class)) {
@@ -2210,6 +2263,44 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         }
     }
 
+    public void verifyComponentTypesInSnippet(final VersionedProcessGroup versionedFlow) {
+        final Map<String, Set<BundleCoordinate>> processorClasses = new HashMap<>();
+        for (final Class<?> c : ExtensionManager.getExtensions(Processor.class)) {
+            final String name = c.getName();
+            processorClasses.put(name, ExtensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet()));
+        }
+        verifyProcessorsInVersionedFlow(versionedFlow, processorClasses);
+
+        final Map<String, Set<BundleCoordinate>> controllerServiceClasses = new HashMap<>();
+        for (final Class<?> c : ExtensionManager.getExtensions(ControllerService.class)) {
+            final String name = c.getName();
+            controllerServiceClasses.put(name, ExtensionManager.getBundles(name).stream().map(bundle -> bundle.getBundleDetails().getCoordinate()).collect(Collectors.toSet()));
+        }
+        verifyControllerServicesInVersionedFlow(versionedFlow, controllerServiceClasses);
+
+        final Set<String> prioritizerClasses = new HashSet<>();
+        for (final Class<?> c : ExtensionManager.getExtensions(FlowFilePrioritizer.class)) {
+            prioritizerClasses.add(c.getName());
+        }
+
+        final Set<VersionedConnection> allConns = new HashSet<>();
+        allConns.addAll(versionedFlow.getConnections());
+        for (final VersionedProcessGroup childGroup : versionedFlow.getProcessGroups()) {
+            allConns.addAll(findAllConnections(childGroup));
+        }
+
+        for (final VersionedConnection conn : allConns) {
+            final List<String> prioritizers = conn.getPrioritizers();
+            if (prioritizers != null) {
+                for (final String prioritizer : prioritizers) {
+                    if (!prioritizerClasses.contains(prioritizer)) {
+                        throw new IllegalStateException("Invalid FlowFile Prioritizer Type: " + prioritizer);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * <p>
      * Verifies that the given DTO is valid, according to the following:
@@ -2270,6 +2361,18 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         return conns;
     }
 
+    private Set<VersionedConnection> findAllConnections(final VersionedProcessGroup group) {
+        final Set<VersionedConnection> conns = new HashSet<>();
+        for (final VersionedConnection connection : group.getConnections()) {
+            conns.add(connection);
+        }
+
+        for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
+            conns.addAll(findAllConnections(childGroup));
+        }
+        return conns;
+    }
+
     //
     // Processor access
     //

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
index e879e38..5a7aeec 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java
@@ -85,6 +85,7 @@ import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.logging.LogLevel;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.SimpleProcessLogger;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -184,7 +185,10 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
         try {
             if (flowAlreadySynchronized) {
                 existingFlow = toBytes(controller);
-                existingFlowEmpty = controller.getGroup(controller.getRootGroupId()).isEmpty() && controller.getAllReportingTasks().isEmpty() && controller.getAllControllerServices().isEmpty();
+                existingFlowEmpty = controller.getGroup(controller.getRootGroupId()).isEmpty()
+                    && controller.getAllReportingTasks().isEmpty()
+                    && controller.getAllControllerServices().isEmpty()
+                    && controller.getFlowRegistryClient().getRegistryIdentifiers().isEmpty();
             } else {
                 existingFlow = readFlowFromDisk();
                 if (existingFlow == null || existingFlow.length == 0) {
@@ -220,10 +224,22 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
                         unrootedControllerServiceElements = DomUtils.getChildElementsByTagName(controllerServicesElement, "controllerService");
                     }
 
+                    final boolean registriesPresent;
+                    final Element registriesElement = DomUtils.getChild(rootElement, "registries");
+                    if (registriesElement == null) {
+                        registriesPresent = false;
+                    } else {
+                        final List<Element> flowRegistryElems = DomUtils.getChildElementsByTagName(registriesElement, "flowRegistry");
+                        registriesPresent = !flowRegistryElems.isEmpty();
+                    }
+
                     logger.trace("Parsing process group from DOM");
                     final Element rootGroupElement = (Element) rootElement.getElementsByTagName("rootGroup").item(0);
                     final ProcessGroupDTO rootGroupDto = FlowFromDOMFactory.getProcessGroup(null, rootGroupElement, encryptor, encodingVersion);
-                    existingFlowEmpty = taskElements.isEmpty() && unrootedControllerServiceElements.isEmpty() && isEmpty(rootGroupDto);
+                    existingFlowEmpty = taskElements.isEmpty()
+                        && unrootedControllerServiceElements.isEmpty()
+                        && isEmpty(rootGroupDto)
+                        && registriesPresent;
                     logger.debug("Existing Flow Empty = {}", existingFlowEmpty);
                 }
             }
@@ -318,6 +334,22 @@ public class StandardFlowSynchronizer implements FlowSynchronizer {
                     // get the root group XML element
                     final Element rootGroupElement = (Element) rootElement.getElementsByTagName("rootGroup").item(0);
 
+                    if (!flowAlreadySynchronized || existingFlowEmpty) {
+                        final Element registriesElement = DomUtils.getChild(rootElement, "registries");
+                        if (registriesElement != null) {
+                            final List<Element> flowRegistryElems = DomUtils.getChildElementsByTagName(registriesElement, "flowRegistry");
+                            for (final Element flowRegistryElement : flowRegistryElems) {
+                                final String registryId = getString(flowRegistryElement, "id");
+                                final String registryName = getString(flowRegistryElement, "name");
+                                final String registryUrl = getString(flowRegistryElement, "url");
+                                final String description = getString(flowRegistryElement, "description");
+
+                                final FlowRegistryClient client = controller.getFlowRegistryClient();
+                                client.addFlowRegistry(registryId, registryName, registryUrl, description);
+                            }
+                        }
+                    }
+
                     // if this controller isn't initialized or its empty, add the root group, otherwise update
                     final ProcessGroup rootGroup;
                     if (!flowAlreadySynchronized || existingFlowEmpty) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
index ecf2438..f921bc6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
@@ -39,6 +39,8 @@ import org.apache.nifi.persistence.TemplateSerializer;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.registry.VariableDescriptor;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
@@ -74,7 +76,7 @@ import java.util.concurrent.TimeUnit;
  */
 public class StandardFlowSerializer implements FlowSerializer {
 
-    private static final String MAX_ENCODING_VERSION = "1.2";
+    private static final String MAX_ENCODING_VERSION = "1.3";
 
     private final StringEncryptor encryptor;
 
@@ -98,6 +100,11 @@ public class StandardFlowSerializer implements FlowSerializer {
             doc.appendChild(rootNode);
             addTextElement(rootNode, "maxTimerDrivenThreadCount", controller.getMaxTimerDrivenThreadCount());
             addTextElement(rootNode, "maxEventDrivenThreadCount", controller.getMaxEventDrivenThreadCount());
+
+            final Element registriesElement = doc.createElement("registries");
+            rootNode.appendChild(registriesElement);
+
+            addFlowRegistries(registriesElement, controller.getFlowRegistryClient());
             addProcessGroup(rootNode, controller.getGroup(controller.getRootGroupId()), "rootGroup", scheduledStateLookup);
 
             // Add root-level controller services
@@ -130,6 +137,26 @@ public class StandardFlowSerializer implements FlowSerializer {
         }
     }
 
+    private void addFlowRegistries(final Element parentElement, final FlowRegistryClient registryClient) {
+        for (final String registryId : registryClient.getRegistryIdentifiers()) {
+            final FlowRegistry flowRegistry = registryClient.getFlowRegistry(registryId);
+
+            final Element registryElement = parentElement.getOwnerDocument().createElement("flowRegistry");
+            parentElement.appendChild(registryElement);
+
+            addStringElement(registryElement, "id", flowRegistry.getIdentifier());
+            addStringElement(registryElement, "name", flowRegistry.getName());
+            addStringElement(registryElement, "url", flowRegistry.getURL());
+            addStringElement(registryElement, "description", flowRegistry.getDescription());
+        }
+    }
+
+    private void addStringElement(final Element parentElement, final String elementName, final String value) {
+        final Element childElement = parentElement.getOwnerDocument().createElement(elementName);
+        childElement.setTextContent(value);
+        parentElement.appendChild(childElement);
+    }
+
     private void addSize(final Element parentElement, final Size size) {
         final Element element = parentElement.getOwnerDocument().createElement("size");
         element.setAttribute("width", String.valueOf(size.getWidth()));

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
index 3aa5084..e1846a0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java
@@ -27,6 +27,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.stream.Stream;
 
 import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilder;
@@ -198,6 +199,21 @@ public class FingerprintFactory {
     }
 
     private StringBuilder addFlowControllerFingerprint(final StringBuilder builder, final Element flowControllerElem, final FlowController controller) {
+        // registries
+        final Element registriesElement = DomUtils.getChild(flowControllerElem, "registries");
+        if (registriesElement == null) {
+            builder.append("NO_VALUE");
+        } else {
+            final List<Element> flowRegistryElems = DomUtils.getChildElementsByTagName(registriesElement, "flowRegistry");
+            if (flowRegistryElems.isEmpty()) {
+                builder.append("NO_VALUE");
+            } else {
+                for (final Element flowRegistryElement : flowRegistryElems) {
+                    addFlowRegistryFingerprint(builder, flowRegistryElement);
+                }
+            }
+        }
+
         // root group
         final Element rootGroupElem = (Element) DomUtils.getChildNodesByTagName(flowControllerElem, "rootGroup").item(0);
         addProcessGroupFingerprint(builder, rootGroupElem, controller);
@@ -265,6 +281,11 @@ public class FingerprintFactory {
         return builder;
     }
 
+    private StringBuilder addFlowRegistryFingerprint(final StringBuilder builder, final Element flowRegistryElement) {
+        Stream.of("id", "name", "url", "description").forEach(elementName -> appendFirstValue(builder, DomUtils.getChildNodesByTagName(flowRegistryElement, elementName)));
+        return builder;
+    }
+
     private StringBuilder addProcessGroupFingerprint(final StringBuilder builder, final Element processGroupElem, final FlowController controller) throws FingerprintException {
         // id
         appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "id"));

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 3b8117b..1d8652e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -72,7 +72,7 @@ import org.apache.nifi.registry.flow.Bundle;
 import org.apache.nifi.registry.flow.ConnectableComponent;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.flow.RemoteFlowCoordinates;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.UnknownResourceException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
@@ -2835,11 +2835,14 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    @Override
     public void disconnectVersionControl() {
         writeLock.lock();
         try {
-            // TODO remove version component ids from each component (until another versioned PG is encountered)
             this.versionControlInfo.set(null);
+
+            // remove version component ids from each component (until another versioned PG is encountered)
+            applyVersionedComponentIds(this, id -> null);
         } finally {
             writeLock.unlock();
         }
@@ -2850,36 +2853,41 @@ public final class StandardProcessGroup implements ProcessGroup {
             return;
         }
 
-        processGroup.setVersionedComponentId(versionedComponentIds.get(processGroup.getIdentifier()));
+        applyVersionedComponentIds(processGroup, versionedComponentIds::get);
+    }
+
+    private void applyVersionedComponentIds(final ProcessGroup processGroup, final Function<String, String> lookup) {
+        processGroup.setVersionedComponentId(lookup.apply(processGroup.getIdentifier()));
 
         processGroup.getConnections().stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
         processGroup.getProcessors().stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
         processGroup.getInputPorts().stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
         processGroup.getOutputPorts().stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
         processGroup.getLabels().stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
         processGroup.getFunnels().stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
         processGroup.getControllerServices(false).stream()
-            .forEach(component -> component.setVersionedComponentId(versionedComponentIds.get(component.getIdentifier())));
+            .forEach(component -> component.setVersionedComponentId(lookup.apply(component.getIdentifier())));
 
         processGroup.getRemoteProcessGroups().stream()
             .forEach(rpg -> {
-                rpg.setVersionedComponentId(versionedComponentIds.get(rpg.getIdentifier()));
+                rpg.setVersionedComponentId(lookup.apply(rpg.getIdentifier()));
 
                 rpg.getInputPorts().stream()
-                    .forEach(port -> port.setVersionedComponentId(versionedComponentIds.get(port.getIdentifier())));
+                    .forEach(port -> port.setVersionedComponentId(lookup.apply(port.getIdentifier())));
 
                 rpg.getOutputPorts().stream()
-                    .forEach(port -> port.setVersionedComponentId(versionedComponentIds.get(port.getIdentifier())));
+                    .forEach(port -> port.setVersionedComponentId(lookup.apply(port.getIdentifier())));
             });
 
         processGroup.getProcessGroups().stream()
-            .forEach(childGroup -> updateVersionedComponentIds(childGroup, versionedComponentIds));
+            .filter(childGroup -> childGroup.getVersionControlInformation() != null)
+            .forEach(childGroup -> applyVersionedComponentIds(childGroup, lookup));
     }
 
 
@@ -2931,10 +2939,10 @@ public final class StandardProcessGroup implements ProcessGroup {
 
 
     @Override
-    public void updateFlow(final VersionedFlowSnapshot proposedSnapshot, final String componentIdSeed, final boolean verifyNotDirty) {
+    public void updateFlow(final VersionedFlowSnapshot proposedSnapshot, final String componentIdSeed, final boolean verifyNotDirty, final boolean updateSettings) {
         writeLock.lock();
         try {
-            verifyCanUpdate(proposedSnapshot, true, verifyNotDirty); // TODO: Should perform more verification... verifyCanDelete, verifyCanUpdate, etc. Recursively if child is under VC also
+            verifyCanUpdate(proposedSnapshot, true, verifyNotDirty);
 
             final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
             final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient());
@@ -2950,15 +2958,15 @@ public final class StandardProcessGroup implements ProcessGroup {
                 .map(diff -> diff.getComponentA() == null ? diff.getComponentB().getIdentifier() : diff.getComponentA().getIdentifier())
                 .collect(Collectors.toSet());
 
-            if (LOG.isDebugEnabled()) {
-                LOG.debug("Updating {} to {}; there are {} differences to take into account: {}", this, proposedSnapshot, flowComparison.getDifferences().size(), flowComparison.getDifferences());
-            } else {
-                // TODO: Remove the actual differences from the info level log. It can be extremely verbose. Is here only for testing purposes becuase it's much more convenient
-                // than having to remember to enable DEBUG level logging every time a full build is done.
-                LOG.info("Updating {} to {}; there are {} differences to take into account: {}", this, proposedSnapshot, flowComparison.getDifferences().size(), flowComparison.getDifferences());
+            if (LOG.isInfoEnabled()) {
+                final String differencesByLine = flowComparison.getDifferences().stream()
+                    .map(FlowDifference::toString)
+                    .collect(Collectors.joining("\n"));
+
+                LOG.info("Updating {} to {}; there are {} differences to take into account:\n{}", this, proposedSnapshot, flowComparison.getDifferences().size(), differencesByLine);
             }
 
-            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false);
+            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings);
         } catch (final ProcessorInstantiationException pie) {
             throw new RuntimeException(pie);
         } finally {
@@ -2968,10 +2976,14 @@ public final class StandardProcessGroup implements ProcessGroup {
 
 
     private void updateProcessGroup(final ProcessGroup group, final VersionedProcessGroup proposed, final String componentIdSeed,
-        final Set<String> updatedVersionedComponentIds, final boolean updatePosition) throws ProcessorInstantiationException {
+        final Set<String> updatedVersionedComponentIds, final boolean updatePosition, final boolean updateName) throws ProcessorInstantiationException {
 
         group.setComments(proposed.getComments());
-        group.setName(proposed.getName());
+
+        if (updateName) {
+            group.setName(proposed.getName());
+        }
+
         if (updatePosition && proposed.getPosition() != null) {
             group.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
         }
@@ -2998,7 +3010,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
         group.setVariables(updatedVariableMap);
 
-        final RemoteFlowCoordinates remoteCoordinates = proposed.getRemoteFlowCoordinates();
+        final VersionedFlowCoordinates remoteCoordinates = proposed.getVersionedFlowCoordinates();
         if (remoteCoordinates != null) {
             final String registryId = flowController.getFlowRegistryClient().getFlowRegistryId(remoteCoordinates.getRegistryUrl());
             final String bucketId = remoteCoordinates.getBucketId();
@@ -3022,7 +3034,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 final ProcessGroup added = addProcessGroup(proposedChildGroup, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else {
-                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true);
+                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName);
                 LOG.info("Updated {}", childGroup);
             }
 
@@ -3136,14 +3148,29 @@ public final class StandardProcessGroup implements ProcessGroup {
         final Map<String, ProcessorNode> processorsByVersionedId = group.getProcessors().stream()
             .collect(Collectors.toMap(component -> component.getVersionedComponentId().orElse(component.getIdentifier()), Function.identity()));
         final Set<String> processorsRemoved = new HashSet<>(processorsByVersionedId.keySet());
+        final Map<ProcessorNode, Set<Relationship>> autoTerminatedRelationships = new HashMap<>();
 
         for (final VersionedProcessor proposedProcessor : proposed.getProcessors()) {
             final ProcessorNode processor = processorsByVersionedId.get(proposedProcessor.getIdentifier());
             if (processor == null) {
                 final ProcessorNode added = addProcessor(proposedProcessor, componentIdSeed);
+
+                final Set<Relationship> proposedAutoTerminated = proposedProcessor.getAutoTerminatedRelationships().stream()
+                    .map(relName -> added.getRelationship(relName))
+                    .collect(Collectors.toSet());
+                autoTerminatedRelationships.put(added, proposedAutoTerminated);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedProcessor.getIdentifier())) {
                 updateProcessor(processor, proposedProcessor);
+
+                final Set<Relationship> proposedAutoTerminated = proposedProcessor.getAutoTerminatedRelationships().stream()
+                    .map(relName -> processor.getRelationship(relName))
+                    .collect(Collectors.toSet());
+
+                if (!processor.getAutoTerminatedRelationships().equals(proposedAutoTerminated)) {
+                    autoTerminatedRelationships.put(processor, proposedAutoTerminated);
+                }
+
                 LOG.info("Updated {}", processor);
             } else {
                 processor.setPosition(new Position(proposedProcessor.getPosition().getX(), proposedProcessor.getPosition().getY()));
@@ -3205,6 +3232,13 @@ public final class StandardProcessGroup implements ProcessGroup {
             group.removeConnection(connection);
         }
 
+        // Once the appropriate connections have been removed, we may now update Processors' auto-terminated relationships.
+        // We cannot do this above, in the 'updateProcessor' call because if a connection is removed and changed to auto-terminated,
+        // then updating this in the updateProcessor call above would attempt to set the Relationship to being auto-terminated while a
+        // Connection for that relationship exists. This will throw an Exception.
+        autoTerminatedRelationships.forEach((proc, rels) -> proc.setAutoTerminatedRelationships(rels));
+
+        // Remove all controller services no longer in use
         for (final String removedVersionedId : controllerServicesRemoved) {
             final ControllerServiceNode service = servicesByVersionedId.get(removedVersionedId);
             LOG.info("Removing {} from {}", service, group);
@@ -3276,7 +3310,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         final ProcessGroup group = flowController.createProcessGroup(generateUuid(componentIdSeed));
         group.setVersionedComponentId(proposed.getIdentifier());
         addProcessGroup(group);
-        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true);
+        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true);
         return group;
     }
 
@@ -3535,10 +3569,6 @@ public final class StandardProcessGroup implements ProcessGroup {
         processor.setYieldPeriod(proposed.getYieldDuration());
         processor.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
 
-        processor.setAutoTerminatedRelationships(proposed.getAutoTerminatedRelationships().stream()
-            .map(relName -> processor.getRelationship(relName))
-            .collect(Collectors.toSet()));
-
         if (!isEqual(processor.getBundleCoordinate(), proposed.getBundle())) {
             final BundleCoordinate newBundleCoordinate = toCoordinate(proposed.getBundle());
             final List<PropertyDescriptor> descriptors = new ArrayList<>(processor.getProperties().keySet());
@@ -3547,6 +3577,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+
     private Map<String, String> populatePropertiesMap(final Map<PropertyDescriptor, String> currentProperties, final Map<String, String> proposedProperties) {
         final Map<String, String> fullPropertyMap = new HashMap<>();
         for (final PropertyDescriptor property : currentProperties.keySet()) {
@@ -3646,7 +3677,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             @Override
             public String getName() {
-                return "Flow Under Version Control";
+                return "Versioned Flow";
             }
         };
 
@@ -3659,7 +3690,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             .findAny()
             .isPresent();
 
-        LOG.debug("There are {} differences between this flow and the versioned snapshot of this flow: {}", differences.size(), differences);
+        LOG.debug("There are {} differences between this Local FLow and the Versioned Flow: {}", differences.size(), differences);
         return Optional.of(modified);
     }
 
@@ -3669,27 +3700,24 @@ public final class StandardProcessGroup implements ProcessGroup {
         readLock.lock();
         try {
             final VersionControlInformation versionControlInfo = getVersionControlInformation();
-            if (versionControlInfo == null) {
-                throw new IllegalStateException("Cannot update the Version of the flow for " + this
-                    + " because the Process Group is not currently under Version Control");
-            }
-
-            if (!versionControlInfo.getFlowIdentifier().equals(updatedFlow.getSnapshotMetadata().getFlowIdentifier())) {
-                throw new IllegalStateException(this + " is under version control but the given flow does not match the flow that this Process Group is synchronized with");
-            }
-
-            if (verifyNotDirty) {
-                final Optional<Boolean> modifiedOption = versionControlInfo.getModified();
-                if (!modifiedOption.isPresent()) {
-                    throw new IllegalStateException(this + " cannot be updated to a different version of the flow because the local flow "
-                        + "has not yet been synchronized with the Flow Registry. The Process Group must be"
-                        + " synched with the Flow Registry before continuing. This will happen periodically in the background, so please try the request again later");
+            if (versionControlInfo != null) {
+                if (!versionControlInfo.getFlowIdentifier().equals(updatedFlow.getSnapshotMetadata().getFlowIdentifier())) {
+                    throw new IllegalStateException(this + " is under version control but the given flow does not match the flow that this Process Group is synchronized with");
                 }
 
-                if (Boolean.TRUE.equals(modifiedOption.get())) {
-                    throw new IllegalStateException("Cannot change the Version of the flow for " + this
-                        + " because the Process Group has been modified since it was last synchronized with the Flow Registry. The Process Group must be"
-                        + " restored to its original form before changing the version");
+                if (verifyNotDirty) {
+                    final Optional<Boolean> modifiedOption = versionControlInfo.getModified();
+                    if (!modifiedOption.isPresent()) {
+                        throw new IllegalStateException(this + " cannot be updated to a different version of the flow because the local flow "
+                            + "has not yet been synchronized with the Flow Registry. The Process Group must be"
+                            + " synched with the Flow Registry before continuing. This will happen periodically in the background, so please try the request again later");
+                    }
+
+                    if (Boolean.TRUE.equals(modifiedOption.get())) {
+                        throw new IllegalStateException("Cannot change the Version of the flow for " + this
+                            + " because the Process Group has been modified since it was last synchronized with the Flow Registry. The Process Group must be"
+                            + " restored to its original form before changing the version");
+                    }
                 }
             }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
new file mode 100644
index 0000000..da5880c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
@@ -0,0 +1,478 @@
+/*
+ * 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.registry.flow;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.registry.bucket.Bucket;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * A simple file-based implementation of a Flow Registry Client. Rather than interacting
+ * with an actual Flow Registry, this implementation simply reads flows from disk and writes
+ * them to disk. It is not meant for any production use but is available for testing purposes.
+ */
+public class FileBasedFlowRegistry implements FlowRegistry {
+    private final File directory;
+    private final Map<String, Set<String>> flowNamesByBucket = new HashMap<>();
+    private final JsonFactory jsonFactory = new JsonFactory();
+    private final String id;
+    private volatile String name = "Local Registry";
+    private volatile String url = "file:" + (new File("..").getAbsolutePath());
+    private volatile String description = "Default file-based Flow Registry";
+
+    public FileBasedFlowRegistry(final String id, final String url) throws IOException {
+        final URI uri = URI.create(url);
+        if (!uri.getScheme().equalsIgnoreCase("file")) {
+            throw new IllegalArgumentException("Cannot create a File Based Flow Registry with a URL of " + url + "; URL scheme must be 'file'");
+        }
+
+        this.directory = new File(URI.create(url).getPath());
+
+        if (!directory.exists() && !directory.mkdirs()) {
+            throw new IOException("Could not access or create directory " + directory.getAbsolutePath() + " for Flow Registry");
+        }
+
+        this.id = id;
+        this.url = url;
+        recoverBuckets();
+    }
+
+    private void recoverBuckets() throws IOException {
+        final File[] bucketDirs = directory.listFiles();
+        if (bucketDirs == null) {
+            throw new IOException("Could not get listing of directory " + directory);
+        }
+
+        for (final File bucketDir : bucketDirs) {
+            final File[] flowDirs = bucketDir.listFiles();
+            if (flowDirs == null) {
+                throw new IOException("Could not get listing of directory " + bucketDir);
+            }
+
+            final Set<String> flowNames = new HashSet<>();
+            for (final File flowDir : flowDirs) {
+                final File propsFile = new File(flowDir, "flow.properties");
+                if (!propsFile.exists()) {
+                    continue;
+                }
+
+                final Properties properties = new Properties();
+                try (final InputStream in = new FileInputStream(propsFile)) {
+                    properties.load(in);
+                }
+
+                final String flowName = properties.getProperty("name");
+                if (flowName == null) {
+                    continue;
+                }
+
+                flowNames.add(flowName);
+            }
+
+            if (!flowNames.isEmpty()) {
+                flowNamesByBucket.put(bucketDir.getName(), flowNames);
+            }
+        }
+    }
+
+    @Override
+    public String getURL() {
+        return url;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public Set<Bucket> getBuckets(NiFiUser user) throws IOException {
+        final Set<Bucket> buckets = new HashSet<>();
+
+        final File[] bucketDirs = directory.listFiles();
+        if (bucketDirs == null) {
+            throw new IOException("Could not get listing of directory " + directory);
+        }
+
+        for (final File bucketDirectory : bucketDirs) {
+            final String bucketIdentifier = bucketDirectory.getName();
+            final long creation = bucketDirectory.lastModified();
+
+            final Bucket bucket = new Bucket();
+            bucket.setIdentifier(bucketIdentifier);
+            bucket.setName("Bucket '" + bucketIdentifier + "'");
+            bucket.setCreatedTimestamp(creation);
+
+            final Set<VersionedFlow> versionedFlows = new HashSet<>();
+            final File[] flowDirs = bucketDirectory.listFiles();
+            if (flowDirs != null) {
+                for (final File flowDir : flowDirs) {
+                    final String flowIdentifier = flowDir.getName();
+                    try {
+                        final VersionedFlow versionedFlow = getVersionedFlow(bucketIdentifier, flowIdentifier);
+                        versionedFlows.add(versionedFlow);
+                    } catch (UnknownResourceException e) {
+                        continue;
+                    }
+                }
+            }
+
+            bucket.setVersionedFlows(versionedFlows);
+
+            buckets.add(bucket);
+        }
+
+        return buckets;
+    }
+
+
+    @Override
+    public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, UnknownResourceException {
+        Objects.requireNonNull(flow);
+        Objects.requireNonNull(flow.getBucketIdentifier());
+        Objects.requireNonNull(flow.getName());
+
+        // Verify that bucket exists
+        final File bucketDir = new File(directory, flow.getBucketIdentifier());
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
+        }
+
+        // Verify that there is no flow with the same name in that bucket
+        final Set<String> flowNames = flowNamesByBucket.get(flow.getBucketIdentifier());
+        if (flowNames != null && flowNames.contains(flow.getName())) {
+            throw new IllegalArgumentException("Flow with name '" + flow.getName() + "' already exists for Bucket with ID " + flow.getBucketIdentifier());
+        }
+
+        final String flowIdentifier = UUID.randomUUID().toString();
+        final File flowDir = new File(bucketDir, flowIdentifier);
+        if (!flowDir.mkdirs()) {
+            throw new IOException("Failed to create directory " + flowDir + " for new Flow");
+        }
+
+        final File propertiesFile = new File(flowDir, "flow.properties");
+
+        final Properties flowProperties = new Properties();
+        flowProperties.setProperty("name", flow.getName());
+        flowProperties.setProperty("created", String.valueOf(flow.getCreatedTimestamp()));
+        flowProperties.setProperty("description", flow.getDescription());
+        flowProperties.setProperty("lastModified", String.valueOf(flow.getModifiedTimestamp()));
+
+        try (final OutputStream out = new FileOutputStream(propertiesFile)) {
+            flowProperties.store(out, null);
+        }
+
+        final VersionedFlow response = new VersionedFlow();
+        response.setBucketIdentifier(flow.getBucketIdentifier());
+        response.setCreatedTimestamp(flow.getCreatedTimestamp());
+        response.setDescription(flow.getDescription());
+        response.setIdentifier(flowIdentifier);
+        response.setModifiedTimestamp(flow.getModifiedTimestamp());
+        response.setName(flow.getName());
+
+        return response;
+    }
+
+    @Override
+    public synchronized VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments)
+            throws IOException, UnknownResourceException {
+        Objects.requireNonNull(flow);
+        Objects.requireNonNull(flow.getBucketIdentifier());
+        Objects.requireNonNull(flow.getName());
+        Objects.requireNonNull(snapshot);
+
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, flow.getBucketIdentifier());
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flow.getIdentifier());
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flow.getIdentifier() + " exists for Bucket with ID " + flow.getBucketIdentifier());
+        }
+
+        final File[] versionDirs = flowDir.listFiles();
+        if (versionDirs == null) {
+            throw new IOException("Unable to perform listing of directory " + flowDir);
+        }
+
+        int maxVersion = 0;
+        for (final File versionDir : versionDirs) {
+            final String versionName = versionDir.getName();
+
+            final int version;
+            try {
+                version = Integer.parseInt(versionName);
+            } catch (final NumberFormatException nfe) {
+                continue;
+            }
+
+            if (version > maxVersion) {
+                maxVersion = version;
+            }
+        }
+
+        final int snapshotVersion = maxVersion + 1;
+        final File snapshotDir = new File(flowDir, String.valueOf(snapshotVersion));
+        if (!snapshotDir.mkdir()) {
+            throw new IOException("Could not create directory " + snapshotDir);
+        }
+
+        final File contentsFile = new File(snapshotDir, "flow.xml");
+
+        try (final OutputStream out = new FileOutputStream(contentsFile);
+            final JsonGenerator generator = jsonFactory.createGenerator(out)) {
+            generator.setCodec(new ObjectMapper());
+            generator.setPrettyPrinter(new DefaultPrettyPrinter());
+            generator.writeObject(snapshot);
+        }
+
+        final Properties snapshotProperties = new Properties();
+        snapshotProperties.setProperty("comments", comments);
+        snapshotProperties.setProperty("name", flow.getName());
+        final File snapshotPropsFile = new File(snapshotDir, "snapshot.properties");
+        try (final OutputStream out = new FileOutputStream(snapshotPropsFile)) {
+            snapshotProperties.store(out, null);
+        }
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier());
+        snapshotMetadata.setComments(comments);
+        snapshotMetadata.setFlowIdentifier(flow.getIdentifier());
+        snapshotMetadata.setFlowName(flow.getName());
+        snapshotMetadata.setTimestamp(System.currentTimeMillis());
+        snapshotMetadata.setVersion(snapshotVersion);
+
+        final VersionedFlowSnapshot response = new VersionedFlowSnapshot();
+        response.setSnapshotMetadata(snapshotMetadata);
+        response.setFlowContents(snapshot);
+        return response;
+    }
+
+    @Override
+    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, bucketId);
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flowId);
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + bucketId);
+        }
+
+        final File[] versionDirs = flowDir.listFiles();
+        if (versionDirs == null) {
+            throw new IOException("Unable to perform listing of directory " + flowDir);
+        }
+
+        int maxVersion = 0;
+        for (final File versionDir : versionDirs) {
+            final String versionName = versionDir.getName();
+
+            final int version;
+            try {
+                version = Integer.parseInt(versionName);
+            } catch (final NumberFormatException nfe) {
+                continue;
+            }
+
+            if (version > maxVersion) {
+                maxVersion = version;
+            }
+        }
+
+        return maxVersion;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, int version) throws IOException, UnknownResourceException {
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, bucketId);
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flowId);
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
+        }
+
+        final File versionDir = new File(flowDir, String.valueOf(version));
+        if (!versionDir.exists()) {
+            throw new UnknownResourceException("Flow with ID " + flowId + " in Bucket with ID " + bucketId + " does not contain a snapshot with version " + version);
+        }
+
+        final File contentsFile = new File(versionDir, "flow.xml");
+
+        final VersionedProcessGroup processGroup;
+        try (final JsonParser parser = jsonFactory.createParser(contentsFile)) {
+            final ObjectMapper mapper = new ObjectMapper();
+            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+            parser.setCodec(mapper);
+            processGroup = parser.readValueAs(VersionedProcessGroup.class);
+        }
+
+        final Properties properties = new Properties();
+        final File snapshotPropsFile = new File(versionDir, "snapshot.properties");
+        try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
+            properties.load(in);
+        }
+
+        final String comments = properties.getProperty("comments");
+        final String flowName = properties.getProperty("name");
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setBucketIdentifier(bucketId);
+        snapshotMetadata.setComments(comments);
+        snapshotMetadata.setFlowIdentifier(flowId);
+        snapshotMetadata.setFlowName(flowName);
+        snapshotMetadata.setTimestamp(System.currentTimeMillis());
+        snapshotMetadata.setVersion(version);
+
+        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
+        snapshot.setFlowContents(processGroup);
+        snapshot.setSnapshotMetadata(snapshotMetadata);
+
+        return snapshot;
+    }
+
+    @Override
+    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+        // Verify that the bucket exists
+        final File bucketDir = new File(directory, bucketId);
+        if (!bucketDir.exists()) {
+            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
+        }
+
+        // Verify that the flow exists
+        final File flowDir = new File(bucketDir, flowId);
+        if (!flowDir.exists()) {
+            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
+        }
+
+        final File flowPropsFile = new File(flowDir, "flow.properties");
+        final Properties flowProperties = new Properties();
+        try (final InputStream in = new FileInputStream(flowPropsFile)) {
+            flowProperties.load(in);
+        }
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setCreatedTimestamp(Long.parseLong(flowProperties.getProperty("created")));
+        flow.setDescription(flowProperties.getProperty("description"));
+        flow.setIdentifier(flowId);
+        flow.setModifiedTimestamp(flowDir.lastModified());
+        flow.setName(flowProperties.getProperty("name"));
+
+        final Comparator<VersionedFlowSnapshotMetadata> versionComparator = (a, b) -> Integer.compare(a.getVersion(), b.getVersion());
+
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshotMetadataSet = new TreeSet<>(versionComparator);
+        flow.setSnapshotMetadata(snapshotMetadataSet);
+
+        final File[] versionDirs = flowDir.listFiles();
+        flow.setVersionCount(versionDirs.length);
+
+        for (final File file : versionDirs) {
+            if (!file.isDirectory()) {
+                continue;
+            }
+
+            int version;
+            try {
+                version = Integer.parseInt(file.getName());
+            } catch (final NumberFormatException nfe) {
+                // not a version. skip.
+                continue;
+            }
+
+            final File snapshotPropsFile = new File(file, "snapshot.properties");
+            final Properties snapshotProperties = new Properties();
+            try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
+                snapshotProperties.load(in);
+            }
+
+            final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
+            metadata.setBucketIdentifier(bucketId);
+            metadata.setComments(snapshotProperties.getProperty("comments"));
+            metadata.setFlowIdentifier(flowId);
+            metadata.setFlowName(snapshotProperties.getProperty("name"));
+            metadata.setTimestamp(file.lastModified());
+            metadata.setVersion(version);
+
+            snapshotMetadataSet.add(metadata);
+        }
+
+        return flow;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return id;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Override
+    public void setURL(String url) {
+        this.url = url;
+    }
+
+    @Override
+    public void setName(String name) {
+        this.name = name;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
deleted file mode 100644
index 2cc39c6..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistryClient.java
+++ /dev/null
@@ -1,435 +0,0 @@
-/*
- * 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.registry.flow;
-
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.nifi.authorization.user.NiFiUser;
-import org.apache.nifi.registry.bucket.Bucket;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.UUID;
-
-/**
- * A simple file-based implementation of a Flow Registry Client. Rather than interacting
- * with an actual Flow Registry, this implementation simply reads flows from disk and writes
- * them to disk. It is not meant for any production use but is available for testing purposes.
- */
-public class FileBasedFlowRegistryClient implements FlowRegistryClient, FlowRegistry {
-    private final File directory;
-    private final Map<String, Set<String>> flowNamesByBucket = new HashMap<>();
-    private final JsonFactory jsonFactory = new JsonFactory();
-
-    public FileBasedFlowRegistryClient(final File directory) throws IOException {
-        if (!directory.exists() && !directory.mkdirs()) {
-            throw new IOException("Could not access or create directory " + directory.getAbsolutePath() + " for Flow Registry");
-        }
-
-        this.directory = directory;
-        recoverBuckets();
-    }
-
-    private void recoverBuckets() throws IOException {
-        final File[] bucketDirs = directory.listFiles();
-        if (bucketDirs == null) {
-            throw new IOException("Could not get listing of directory " + directory);
-        }
-
-        for (final File bucketDir : bucketDirs) {
-            final File[] flowDirs = bucketDir.listFiles();
-            if (flowDirs == null) {
-                throw new IOException("Could not get listing of directory " + bucketDir);
-            }
-
-            final Set<String> flowNames = new HashSet<>();
-            for (final File flowDir : flowDirs) {
-                final File propsFile = new File(flowDir, "flow.properties");
-                if (!propsFile.exists()) {
-                    continue;
-                }
-
-                final Properties properties = new Properties();
-                try (final InputStream in = new FileInputStream(propsFile)) {
-                    properties.load(in);
-                }
-
-                final String flowName = properties.getProperty("name");
-                if (flowName == null) {
-                    continue;
-                }
-
-                flowNames.add(flowName);
-            }
-
-            if (!flowNames.isEmpty()) {
-                flowNamesByBucket.put(bucketDir.getName(), flowNames);
-            }
-        }
-    }
-
-    @Override
-    public FlowRegistry getFlowRegistry(final String registryId) {
-        if (!"default".equals(registryId)) {
-            return null;
-        }
-
-        return this;
-    }
-
-    @Override
-    public String getURL() {
-        return directory.toURI().toString();
-    }
-
-    @Override
-    public String getName() {
-        return "Local Registry";
-    }
-
-    @Override
-    public Set<Bucket> getBuckets(NiFiUser user) throws IOException {
-        final Set<Bucket> buckets = new HashSet<>();
-
-        final File[] bucketDirs = directory.listFiles();
-        if (bucketDirs == null) {
-            throw new IOException("Could not get listing of directory " + directory);
-        }
-
-        for (final File bucketDirectory : bucketDirs) {
-            final String bucketIdentifier = bucketDirectory.getName();
-            final long creation = bucketDirectory.lastModified();
-
-            final Bucket bucket = new Bucket();
-            bucket.setIdentifier(bucketIdentifier);
-            bucket.setName("Bucket '" + bucketIdentifier + "'");
-            bucket.setCreatedTimestamp(creation);
-
-            buckets.add(bucket);
-        }
-
-        return buckets;
-    }
-
-    @Override
-    public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, UnknownResourceException {
-        Objects.requireNonNull(flow);
-        Objects.requireNonNull(flow.getBucketIdentifier());
-        Objects.requireNonNull(flow.getName());
-
-        // Verify that bucket exists
-        final File bucketDir = new File(directory, flow.getBucketIdentifier());
-        if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
-        }
-
-        // Verify that there is no flow with the same name in that bucket
-        final Set<String> flowNames = flowNamesByBucket.get(flow.getBucketIdentifier());
-        if (flowNames != null && flowNames.contains(flow.getName())) {
-            throw new IllegalArgumentException("Flow with name '" + flow.getName() + "' already exists for Bucket with ID " + flow.getBucketIdentifier());
-        }
-
-        final String flowIdentifier = UUID.randomUUID().toString();
-        final File flowDir = new File(bucketDir, flowIdentifier);
-        if (!flowDir.mkdirs()) {
-            throw new IOException("Failed to create directory " + flowDir + " for new Flow");
-        }
-
-        final File propertiesFile = new File(flowDir, "flow.properties");
-
-        final Properties flowProperties = new Properties();
-        flowProperties.setProperty("name", flow.getName());
-        flowProperties.setProperty("created", String.valueOf(flow.getCreatedTimestamp()));
-        flowProperties.setProperty("description", flow.getDescription());
-        flowProperties.setProperty("lastModified", String.valueOf(flow.getModifiedTimestamp()));
-
-        try (final OutputStream out = new FileOutputStream(propertiesFile)) {
-            flowProperties.store(out, null);
-        }
-
-        final VersionedFlow response = new VersionedFlow();
-        response.setBucketIdentifier(flow.getBucketIdentifier());
-        response.setCreatedTimestamp(flow.getCreatedTimestamp());
-        response.setDescription(flow.getDescription());
-        response.setIdentifier(flowIdentifier);
-        response.setModifiedTimestamp(flow.getModifiedTimestamp());
-        response.setName(flow.getName());
-
-        return response;
-    }
-
-    @Override
-    public synchronized VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments)
-            throws IOException, UnknownResourceException {
-        Objects.requireNonNull(flow);
-        Objects.requireNonNull(flow.getBucketIdentifier());
-        Objects.requireNonNull(flow.getName());
-        Objects.requireNonNull(snapshot);
-
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, flow.getBucketIdentifier());
-        if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + flow.getBucketIdentifier());
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flow.getIdentifier());
-        if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flow.getIdentifier() + " exists for Bucket with ID " + flow.getBucketIdentifier());
-        }
-
-        final File[] versionDirs = flowDir.listFiles();
-        if (versionDirs == null) {
-            throw new IOException("Unable to perform listing of directory " + flowDir);
-        }
-
-        int maxVersion = 0;
-        for (final File versionDir : versionDirs) {
-            final String versionName = versionDir.getName();
-
-            final int version;
-            try {
-                version = Integer.parseInt(versionName);
-            } catch (final NumberFormatException nfe) {
-                continue;
-            }
-
-            if (version > maxVersion) {
-                maxVersion = version;
-            }
-        }
-
-        final int snapshotVersion = maxVersion + 1;
-        final File snapshotDir = new File(flowDir, String.valueOf(snapshotVersion));
-        if (!snapshotDir.mkdir()) {
-            throw new IOException("Could not create directory " + snapshotDir);
-        }
-
-        final File contentsFile = new File(snapshotDir, "flow.xml");
-
-        try (final OutputStream out = new FileOutputStream(contentsFile);
-            final JsonGenerator generator = jsonFactory.createJsonGenerator(out)) {
-            generator.setCodec(new ObjectMapper());
-            generator.setPrettyPrinter(new DefaultPrettyPrinter());
-            generator.writeObject(snapshot);
-        }
-
-        final Properties snapshotProperties = new Properties();
-        snapshotProperties.setProperty("comments", comments);
-        snapshotProperties.setProperty("name", flow.getName());
-        final File snapshotPropsFile = new File(snapshotDir, "snapshot.properties");
-        try (final OutputStream out = new FileOutputStream(snapshotPropsFile)) {
-            snapshotProperties.store(out, null);
-        }
-
-        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
-        snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier());
-        snapshotMetadata.setComments(comments);
-        snapshotMetadata.setFlowIdentifier(flow.getIdentifier());
-        snapshotMetadata.setFlowName(flow.getName());
-        snapshotMetadata.setTimestamp(System.currentTimeMillis());
-        snapshotMetadata.setVersion(snapshotVersion);
-
-        final VersionedFlowSnapshot response = new VersionedFlowSnapshot();
-        response.setSnapshotMetadata(snapshotMetadata);
-        response.setFlowContents(snapshot);
-        return response;
-    }
-
-    @Override
-    public Set<String> getRegistryIdentifiers() {
-        return Collections.singleton("default");
-    }
-
-    @Override
-    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, bucketId);
-        if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flowId);
-        if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + bucketId);
-        }
-
-        final File[] versionDirs = flowDir.listFiles();
-        if (versionDirs == null) {
-            throw new IOException("Unable to perform listing of directory " + flowDir);
-        }
-
-        int maxVersion = 0;
-        for (final File versionDir : versionDirs) {
-            final String versionName = versionDir.getName();
-
-            final int version;
-            try {
-                version = Integer.parseInt(versionName);
-            } catch (final NumberFormatException nfe) {
-                continue;
-            }
-
-            if (version > maxVersion) {
-                maxVersion = version;
-            }
-        }
-
-        return maxVersion;
-    }
-
-    @Override
-    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, int version) throws IOException, UnknownResourceException {
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, bucketId);
-        if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flowId);
-        if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
-        }
-
-        final File versionDir = new File(flowDir, String.valueOf(version));
-        if (!versionDir.exists()) {
-            throw new UnknownResourceException("Flow with ID " + flowId + " in Bucket with ID " + bucketId + " does not contain a snapshot with version " + version);
-        }
-
-        final File contentsFile = new File(versionDir, "flow.xml");
-
-        final VersionedProcessGroup processGroup;
-        try (final JsonParser parser = jsonFactory.createParser(contentsFile)) {
-            final ObjectMapper mapper = new ObjectMapper();
-            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
-            parser.setCodec(mapper);
-            processGroup = parser.readValueAs(VersionedProcessGroup.class);
-        }
-
-        final Properties properties = new Properties();
-        final File snapshotPropsFile = new File(versionDir, "snapshot.properties");
-        try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
-            properties.load(in);
-        }
-
-        final String comments = properties.getProperty("comments");
-        final String flowName = properties.getProperty("name");
-
-        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
-        snapshotMetadata.setBucketIdentifier(bucketId);
-        snapshotMetadata.setComments(comments);
-        snapshotMetadata.setFlowIdentifier(flowId);
-        snapshotMetadata.setFlowName(flowName);
-        snapshotMetadata.setTimestamp(System.currentTimeMillis());
-        snapshotMetadata.setVersion(version);
-
-        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
-        snapshot.setFlowContents(processGroup);
-        snapshot.setSnapshotMetadata(snapshotMetadata);
-
-        return snapshot;
-    }
-
-    @Override
-    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, UnknownResourceException {
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, bucketId);
-        if (!bucketDir.exists()) {
-            throw new UnknownResourceException("No bucket exists with ID " + bucketId);
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flowId);
-        if (!flowDir.exists()) {
-            throw new UnknownResourceException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
-        }
-
-        final File flowPropsFile = new File(flowDir, "flow.properties");
-        final Properties flowProperties = new Properties();
-        try (final InputStream in = new FileInputStream(flowPropsFile)) {
-            flowProperties.load(in);
-        }
-
-        final VersionedFlow flow = new VersionedFlow();
-        flow.setBucketIdentifier(bucketId);
-        flow.setCreatedTimestamp(Long.parseLong(flowProperties.getProperty("created")));
-        flow.setDescription(flowProperties.getProperty("description"));
-        flow.setIdentifier(flowId);
-        flow.setModifiedTimestamp(flowDir.lastModified());
-        flow.setName(flowProperties.getProperty("name"));
-
-        final Comparator<VersionedFlowSnapshotMetadata> versionComparator = (a, b) -> Integer.compare(a.getVersion(), b.getVersion());
-
-        final SortedSet<VersionedFlowSnapshotMetadata> snapshotMetadataSet = new TreeSet<>(versionComparator);
-        flow.setSnapshotMetadata(snapshotMetadataSet);
-
-        final File[] versionDirs = flowDir.listFiles();
-        for (final File file : versionDirs) {
-            if (!file.isDirectory()) {
-                continue;
-            }
-
-            int version;
-            try {
-                version = Integer.parseInt(file.getName());
-            } catch (final NumberFormatException nfe) {
-                // not a version. skip.
-                continue;
-            }
-
-            final File snapshotPropsFile = new File(file, "snapshot.properties");
-            final Properties snapshotProperties = new Properties();
-            try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
-                snapshotProperties.load(in);
-            }
-
-            final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
-            metadata.setBucketIdentifier(bucketId);
-            metadata.setComments(snapshotProperties.getProperty("comments"));
-            metadata.setFlowIdentifier(flowId);
-            metadata.setFlowName(snapshotProperties.getProperty("name"));
-            metadata.setTimestamp(file.lastModified());
-            metadata.setVersion(version);
-
-            snapshotMetadataSet.add(metadata);
-        }
-
-        return flow;
-    }
-}


[36/50] nifi git commit: NIFI-4436: - Code clean up. - Improved error handling. - Minor UX improvements. - Adding message to indicate that variables do not support sensitive values. - Preventing a user from changing the flow version to the current versio

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 111636b..27004b4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -1095,6 +1095,7 @@
 
                     // up to date current
                     var upToDate = details.select('text.process-group-up-to-date')
+                        .style('visibility', 'visible')
                         .classed('up-to-date', function (d) {
                             return d.component.upToDateCount > 0;
                         })
@@ -1102,6 +1103,7 @@
                             return d.component.upToDateCount === 0;
                         });
                     var upToDateCount = details.select('text.process-group-up-to-date-count')
+                        .style('visibility', 'visible')
                         .attr('x', function () {
                             var updateToDateCountX = parseInt(upToDate.attr('x'), 10);
                             return updateToDateCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
@@ -1112,6 +1114,7 @@
 
                     // update locally modified
                     var locallyModified = details.select('text.process-group-locally-modified')
+                        .style('visibility', 'visible')
                         .classed('locally-modified', function (d) {
                             return d.component.locallyModifiedCount > 0;
                         })
@@ -1123,6 +1126,7 @@
                             return upToDateX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                         });
                     var locallyModifiedCount = details.select('text.process-group-locally-modified-count')
+                        .style('visibility', 'visible')
                         .attr('x', function () {
                             var locallyModifiedCountX = parseInt(locallyModified.attr('x'), 10);
                             return locallyModifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
@@ -1133,6 +1137,7 @@
 
                     // update stale
                     var stale = details.select('text.process-group-stale')
+                        .style('visibility', 'visible')
                         .classed('stale', function (d) {
                             return d.component.staleCount > 0;
                         })
@@ -1144,6 +1149,7 @@
                             return locallyModifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                         });
                     var staleCount = details.select('text.process-group-stale-count')
+                        .style('visibility', 'visible')
                         .attr('x', function () {
                             var staleCountX = parseInt(stale.attr('x'), 10);
                             return staleCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
@@ -1154,6 +1160,7 @@
 
                     // update locally modified and stale
                     var locallyModifiedAndStale = details.select('text.process-group-locally-modified-and-stale')
+                        .style('visibility', 'visible')
                         .classed('locally-modified-and-stale', function (d) {
                             return d.component.locallyModifiedAndStaleCount > 0;
                         })
@@ -1165,6 +1172,7 @@
                             return staleX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                         });
                     var locallyModifiedAndStaleCount = details.select('text.process-group-locally-modified-and-stale-count')
+                        .style('visibility', 'visible')
                         .attr('x', function () {
                             var locallyModifiedAndStaleCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
                             return locallyModifiedAndStaleCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
@@ -1175,6 +1183,7 @@
 
                     // update sync failure
                     var syncFailure = details.select('text.process-group-sync-failure')
+                        .style('visibility', 'visible')
                         .classed('sync-failure', function (d) {
                             return d.component.syncFailureCount > 0;
                         })
@@ -1186,6 +1195,7 @@
                             return syncFailureX + Math.round(locallyModifiedAndStaleCount.node().getComputedTextLength()) + CONTENTS_SPACER - 2;
                         });
                     details.select('text.process-group-sync-failure-count')
+                        .style('visibility', 'visible')
                         .attr('x', function () {
                             var syncFailureCountX = parseInt(syncFailure.attr('x'), 10);
                             return syncFailureCountX + Math.round(syncFailure.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
@@ -1195,20 +1205,22 @@
                         });
                 } else {
                     // update version control information
-                    processGroup.select('text.version-control').style('visibility', false).text('');
+                    processGroup.select('text.version-control').style('visibility', 'hidden');
 
                     // clear the process group comments
-                    processGroup.select('path.component-comments').style('visibility', false);
+                    processGroup.select('path.component-comments').style('visibility', 'hidden');
 
                     // clear the encapsulate versioned pg counts
-                    details.select('text.process-group-up-to-date').style('visibility', false);
-                    details.select('text.process-group-up-to-date-count').style('visibility', false);
-                    details.select('text.process-group-locally-modified').style('visibility', false);
-                    details.select('text.process-group-locally-modified-count').style('visibility', false);
-                    details.select('text.process-group-stale').style('visibility', false);
-                    details.select('text.process-group-stale-count').style('visibility', false);
-                    details.select('text.process-group-locally-modified-and-stale').style('visibility', false);
-                    details.select('text.process-group-locally-modified-and-stale-count').style('visibility', false);
+                    details.select('text.process-group-up-to-date').style('visibility', 'hidden');
+                    details.select('text.process-group-up-to-date-count').style('visibility', 'hidden');
+                    details.select('text.process-group-locally-modified').style('visibility', 'hidden');
+                    details.select('text.process-group-locally-modified-count').style('visibility', 'hidden');
+                    details.select('text.process-group-stale').style('visibility', 'hidden');
+                    details.select('text.process-group-stale-count').style('visibility', 'hidden');
+                    details.select('text.process-group-locally-modified-and-stale').style('visibility', 'hidden');
+                    details.select('text.process-group-locally-modified-and-stale-count').style('visibility', 'hidden');
+                    details.select('text.process-group-sync-failure').style('visibility', 'hidden');
+                    details.select('text.process-group-sync-failure-count').style('visibility', 'hidden');
 
                     // clear the process group name
                     processGroup.select('text.process-group-name')


[27/50] nifi git commit: NIFI-4436: More intelligently flag a ProcessGroup to indicate whether or not it has any local modifications compared to Versioned Flow - Bug fixes - Updated to include status of a Versioned Process Group to include VersionedFlowS

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
index 7bab76d..bdd328c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
@@ -28,7 +28,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.ClassUtils;
@@ -44,6 +43,7 @@ import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.label.Label;
 import org.apache.nifi.controller.queue.FlowFileQueue;
 import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.nar.ExtensionManager;
@@ -59,15 +59,16 @@ import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.PortType;
 import org.apache.nifi.registry.flow.Position;
-import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedControllerService;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionedFunnel;
 import org.apache.nifi.registry.flow.VersionedLabel;
 import org.apache.nifi.registry.flow.VersionedPort;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
 import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -80,56 +81,16 @@ public class NiFiRegistryFlowMapper {
     // created before attempting to create the connection, where the ConnectableDTO is converted.
     private Map<String, String> versionedComponentIds = new HashMap<>();
 
-    public InstantiatedVersionedProcessGroup mapProcessGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean mapDescendantVersionedFlows) {
+    public InstantiatedVersionedProcessGroup mapProcessGroup(final ProcessGroup group, final ControllerServiceProvider serviceProvider, final FlowRegistryClient registryClient,
+            final boolean mapDescendantVersionedFlows) {
         versionedComponentIds.clear();
-        final InstantiatedVersionedProcessGroup mapped = mapGroup(group, registryClient, true, mapDescendantVersionedFlows);
+        final InstantiatedVersionedProcessGroup mapped = mapGroup(group, serviceProvider, registryClient, true, mapDescendantVersionedFlows);
 
-        populateReferencedAncestorServices(group, mapped);
         populateReferencedAncestorVariables(group, mapped);
 
         return mapped;
     }
 
-    private void populateReferencedAncestorServices(final ProcessGroup group, final VersionedProcessGroup versionedGroup) {
-        final Set<ControllerServiceNode> ancestorControllerServices = group.getControllerServices(true);
-        ancestorControllerServices.remove(group.getControllerServices(false));
-        final Map<String, ControllerServiceNode> ancestorServicesById = ancestorControllerServices.stream()
-            .collect(Collectors.toMap(ControllerServiceNode::getIdentifier, Function.identity()));
-
-        final Set<ControllerServiceNode> referenced = new HashSet<>();
-
-        for (final ProcessorNode processor : group.findAllProcessors()) {
-            findReferencedServices(processor, ancestorServicesById, referenced);
-        }
-
-        for (final ControllerServiceNode service : group.findAllControllerServices()) {
-            findReferencedServices(service, ancestorServicesById, referenced);
-        }
-
-        final Set<VersionedControllerService> versionedServices = referenced.stream().map(this::mapControllerService)
-            .collect(Collectors.toCollection(LinkedHashSet::new));
-
-        versionedGroup.getControllerServices().addAll(versionedServices);
-    }
-
-    private Set<ControllerServiceNode> findReferencedServices(final ConfiguredComponent component, final Map<String, ControllerServiceNode> ancestorServicesById,
-        final Set<ControllerServiceNode> referenced) {
-
-        for (final Map.Entry<PropertyDescriptor, String> entry : component.getProperties().entrySet()) {
-            final PropertyDescriptor descriptor = entry.getKey();
-            if (descriptor.getControllerServiceDefinition() != null) {
-                final String serviceId = entry.getValue();
-                final ControllerServiceNode serviceNode = ancestorServicesById.get(serviceId);
-                if (serviceNode != null) {
-                    referenced.add(serviceNode);
-                    referenced.addAll(findReferencedServices(serviceNode, ancestorServicesById, referenced));
-                }
-            }
-        }
-
-        return referenced;
-    }
-
     private void populateReferencedAncestorVariables(final ProcessGroup group, final VersionedProcessGroup versionedGroup) {
         final Set<String> ancestorVariableNames = new HashSet<>();
         populateVariableNames(group.getParent(), ancestorVariableNames);
@@ -167,7 +128,9 @@ public class NiFiRegistryFlowMapper {
     }
 
 
-    private InstantiatedVersionedProcessGroup mapGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean topLevel, final boolean mapDescendantVersionedFlows) {
+    private InstantiatedVersionedProcessGroup mapGroup(final ProcessGroup group, final ControllerServiceProvider serviceLookup, final FlowRegistryClient registryClient,
+            final boolean topLevel, final boolean mapDescendantVersionedFlows) {
+
         final InstantiatedVersionedProcessGroup versionedGroup = new InstantiatedVersionedProcessGroup(group.getIdentifier(), group.getProcessGroupIdentifier());
         versionedGroup.setIdentifier(getId(group.getVersionedComponentId(), group.getIdentifier()));
         versionedGroup.setGroupIdentifier(getGroupId(group.getProcessGroupIdentifier()));
@@ -212,7 +175,7 @@ public class NiFiRegistryFlowMapper {
         }
 
         versionedGroup.setControllerServices(group.getControllerServices(false).stream()
-            .map(this::mapControllerService)
+            .map(service -> mapControllerService(service, serviceLookup))
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setFunnels(group.getFunnels().stream()
@@ -232,7 +195,7 @@ public class NiFiRegistryFlowMapper {
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setProcessors(group.getProcessors().stream()
-            .map(this::mapProcessor)
+            .map(processor -> mapProcessor(processor, serviceLookup))
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setRemoteProcessGroups(group.getRemoteProcessGroups().stream()
@@ -240,7 +203,7 @@ public class NiFiRegistryFlowMapper {
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setProcessGroups(group.getProcessGroups().stream()
-            .map(grp -> mapGroup(grp, registryClient, false, mapDescendantVersionedFlows))
+            .map(grp -> mapGroup(grp, serviceLookup, registryClient, false, mapDescendantVersionedFlows))
             .collect(Collectors.toCollection(LinkedHashSet::new)));
 
         versionedGroup.setConnections(group.getConnections().stream()
@@ -335,7 +298,7 @@ public class NiFiRegistryFlowMapper {
         return component;
     }
 
-    public VersionedControllerService mapControllerService(final ControllerServiceNode controllerService) {
+    public VersionedControllerService mapControllerService(final ControllerServiceNode controllerService, final ControllerServiceProvider serviceProvider) {
         final VersionedControllerService versionedService = new InstantiatedVersionedControllerService(controllerService.getIdentifier(), controllerService.getProcessGroupIdentifier());
         versionedService.setIdentifier(getId(controllerService.getVersionedComponentId(), controllerService.getIdentifier()));
         versionedService.setGroupIdentifier(getGroupId(controllerService.getProcessGroupIdentifier()));
@@ -345,14 +308,16 @@ public class NiFiRegistryFlowMapper {
         versionedService.setComments(controllerService.getComments());
 
         versionedService.setControllerServiceApis(mapControllerServiceApis(controllerService));
-        versionedService.setProperties(mapProperties(controllerService));
+        versionedService.setProperties(mapProperties(controllerService, serviceProvider));
+        versionedService.setPropertyDescriptors(mapPropertyDescriptors(controllerService));
         versionedService.setType(controllerService.getCanonicalClassName());
 
         return versionedService;
     }
 
-    private Map<String, String> mapProperties(final ConfiguredComponent component) {
+    private Map<String, String> mapProperties(final ConfiguredComponent component, final ControllerServiceProvider serviceProvider) {
         final Map<String, String> mapped = new HashMap<>();
+
         component.getProperties().keySet().stream()
             .filter(property -> !property.isSensitive())
             .forEach(property -> {
@@ -360,11 +325,34 @@ public class NiFiRegistryFlowMapper {
                 if (value == null) {
                     value = property.getDefaultValue();
                 }
+
+                if (value != null && property.getControllerServiceDefinition() != null) {
+                    // Property references a Controller Service. Instead of storing the existing value, we want
+                    // to store the Versioned Component ID of the service.
+                    final ControllerServiceNode controllerService = serviceProvider.getControllerServiceNode(value);
+                    if (controllerService != null) {
+                        value = getId(controllerService.getVersionedComponentId(), controllerService.getIdentifier());
+                    }
+                }
+
                 mapped.put(property.getName(), value);
             });
+
         return mapped;
     }
 
+    private Map<String, VersionedPropertyDescriptor> mapPropertyDescriptors(final ConfiguredComponent component) {
+        final Map<String, VersionedPropertyDescriptor> descriptors = new HashMap<>();
+        for (final PropertyDescriptor descriptor : component.getProperties().keySet()) {
+            final VersionedPropertyDescriptor versionedDescriptor = new VersionedPropertyDescriptor();
+            versionedDescriptor.setName(descriptor.getName());
+            versionedDescriptor.setDisplayName(descriptor.getDisplayName());
+            versionedDescriptor.setIdentifiesControllerService(descriptor.getControllerServiceDefinition() != null);
+            descriptors.put(descriptor.getName(), versionedDescriptor);
+        }
+        return descriptors;
+    }
+
     private Bundle mapBundle(final BundleCoordinate coordinate) {
         final Bundle versionedBundle = new Bundle();
         versionedBundle.setGroup(coordinate.getGroup());
@@ -441,7 +429,7 @@ public class NiFiRegistryFlowMapper {
         return position;
     }
 
-    public VersionedProcessor mapProcessor(final ProcessorNode procNode) {
+    public VersionedProcessor mapProcessor(final ProcessorNode procNode, final ControllerServiceProvider serviceProvider) {
         final VersionedProcessor processor = new InstantiatedVersionedProcessor(procNode.getIdentifier(), procNode.getProcessGroupIdentifier());
         processor.setIdentifier(getId(procNode.getVersionedComponentId(), procNode.getIdentifier()));
         processor.setGroupIdentifier(getGroupId(procNode.getProcessGroupIdentifier()));
@@ -456,7 +444,8 @@ public class NiFiRegistryFlowMapper {
         processor.setName(procNode.getName());
         processor.setPenaltyDuration(procNode.getPenalizationPeriod());
         processor.setPosition(mapPosition(procNode.getPosition()));
-        processor.setProperties(mapProperties(procNode));
+        processor.setProperties(mapProperties(procNode, serviceProvider));
+        processor.setPropertyDescriptors(mapPropertyDescriptors(procNode));
         processor.setRunDurationMillis(procNode.getRunDuration(TimeUnit.MILLISECONDS));
         processor.setSchedulingPeriod(procNode.getSchedulingPeriod());
         processor.setSchedulingStrategy(procNode.getSchedulingStrategy().name());

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index 67710fe..ef05a1b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -178,7 +178,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
         };
 
         final Runnable checkAuthorizations = new InitializationTask();
-        backgroundThreadExecutor = new FlowEngine(1, "Remote Process Group " + id);
+        backgroundThreadExecutor = new FlowEngine(1, "Remote Process Group " + id, true);
         backgroundThreadExecutor.scheduleWithFixedDelay(checkAuthorizations, 5L, 30L, TimeUnit.SECONDS);
         backgroundThreadExecutor.submit(() -> {
             try {
@@ -435,11 +435,13 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
      * and started.
      *
      * @param ports the new ports
+     * @param pruneUnusedPorts if true, any ports that are not included in the given set of ports
+     *            and that do not have any incoming connections will be removed.
      *
      * @throws NullPointerException if the given argument is null
      */
     @Override
-    public void setInputPorts(final Set<RemoteProcessGroupPortDescriptor> ports) {
+    public void setInputPorts(final Set<RemoteProcessGroupPortDescriptor> ports, final boolean pruneUnusedPorts) {
         writeLock.lock();
         try {
             final List<String> newPortTargetIds = new ArrayList<>();
@@ -478,16 +480,18 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
 
             // See if we have any ports that no longer exist; cannot be removed within the loop because it would cause
             // a ConcurrentModificationException.
-            final Iterator<StandardRemoteGroupPort> itr = inputPorts.values().iterator();
-            while (itr.hasNext()) {
-                final StandardRemoteGroupPort port = itr.next();
-                if (!newPortTargetIds.contains(port.getTargetIdentifier())) {
-                    port.setTargetExists(false);
-                    port.setTargetRunning(false);
-
-                    // If port has incoming connection, it will be cleaned up when the connection is removed
-                    if (!port.hasIncomingConnection()) {
-                        itr.remove();
+            if (pruneUnusedPorts) {
+                final Iterator<StandardRemoteGroupPort> itr = inputPorts.values().iterator();
+                while (itr.hasNext()) {
+                    final StandardRemoteGroupPort port = itr.next();
+                    if (!newPortTargetIds.contains(port.getTargetIdentifier())) {
+                        port.setTargetExists(false);
+                        port.setTargetRunning(false);
+
+                        // If port has incoming connection, it will be cleaned up when the connection is removed
+                        if (!port.hasIncomingConnection()) {
+                            itr.remove();
+                        }
                     }
                 }
             }
@@ -521,11 +525,13 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
      * and started.
      *
      * @param ports the new ports
+     * @param pruneUnusedPorts if true, will remove any ports that are not in the given list and that have
+     *            no outgoing connections
      *
      * @throws NullPointerException if the given argument is null
      */
     @Override
-    public void setOutputPorts(final Set<RemoteProcessGroupPortDescriptor> ports) {
+    public void setOutputPorts(final Set<RemoteProcessGroupPortDescriptor> ports, final boolean pruneUnusedPorts) {
         writeLock.lock();
         try {
             final List<String> newPortTargetIds = new ArrayList<>();
@@ -535,7 +541,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
                 final Map<String, StandardRemoteGroupPort> outputPortByTargetId = outputPorts.values().stream()
                     .collect(Collectors.toMap(StandardRemoteGroupPort::getTargetIdentifier, Function.identity()));
 
-                final Map<String, StandardRemoteGroupPort> outputPortByName = inputPorts.values().stream()
+                final Map<String, StandardRemoteGroupPort> outputPortByName = outputPorts.values().stream()
                     .collect(Collectors.toMap(StandardRemoteGroupPort::getName, Function.identity()));
 
                 // Check if we have a matching port already and add the port if not. We determine a matching port
@@ -564,16 +570,18 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
 
             // See if we have any ports that no longer exist; cannot be removed within the loop because it would cause
             // a ConcurrentModificationException.
-            final Iterator<StandardRemoteGroupPort> itr = outputPorts.values().iterator();
-            while (itr.hasNext()) {
-                final StandardRemoteGroupPort port = itr.next();
-                if (!newPortTargetIds.contains(port.getTargetIdentifier())) {
-                    port.setTargetExists(false);
-                    port.setTargetRunning(false);
-
-                    // If port has connections, it will be cleaned up when connections are removed
-                    if (port.getConnections().isEmpty()) {
-                        itr.remove();
+            if (pruneUnusedPorts) {
+                final Iterator<StandardRemoteGroupPort> itr = outputPorts.values().iterator();
+                while (itr.hasNext()) {
+                    final StandardRemoteGroupPort port = itr.next();
+                    if (!newPortTargetIds.contains(port.getTargetIdentifier())) {
+                        port.setTargetExists(false);
+                        port.setTargetRunning(false);
+
+                        // If port has connections, it will be cleaned up when connections are removed
+                        if (port.getConnections().isEmpty()) {
+                            itr.remove();
+                        }
                     }
                 }
             }
@@ -617,53 +625,6 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
         }
     }
 
-    @Override
-    public void removeAllNonExistentPorts() {
-        writeLock.lock();
-        try {
-            final Set<String> inputPortIds = new HashSet<>();
-            final Set<String> outputPortIds = new HashSet<>();
-
-            for (final Map.Entry<String, StandardRemoteGroupPort> entry : inputPorts.entrySet()) {
-                final RemoteGroupPort port = entry.getValue();
-
-                if (port.getTargetExists()) {
-                    continue;
-                }
-
-                // If there's a connection, we don't remove it.
-                if (port.hasIncomingConnection()) {
-                    continue;
-                }
-
-                inputPortIds.add(entry.getKey());
-            }
-
-            for (final Map.Entry<String, StandardRemoteGroupPort> entry : outputPorts.entrySet()) {
-                final RemoteGroupPort port = entry.getValue();
-
-                if (port.getTargetExists()) {
-                    continue;
-                }
-
-                // If there's a connection, we don't remove it.
-                if (!port.getConnections().isEmpty()) {
-                    continue;
-                }
-
-                outputPortIds.add(entry.getKey());
-            }
-
-            for (final String id : inputPortIds) {
-                inputPorts.remove(id);
-            }
-            for (final String id : outputPortIds) {
-                outputPorts.remove(id);
-            }
-        } finally {
-            writeLock.unlock();
-        }
-    }
 
     /**
      * Adds an Output Port to this Remote Process Group that is described by
@@ -865,35 +826,16 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
             try (final SiteToSiteRestApiClient apiClient = getSiteToSiteRestApiClient()) {
                 dto = apiClient.getController(targetUris);
             } catch (IOException e) {
-                writeLock.lock();
-                try {
-                    for (final Iterator<StandardRemoteGroupPort> iter = inputPorts.values().iterator(); iter.hasNext();) {
-                        final StandardRemoteGroupPort inputPort = iter.next();
-                        if (!inputPort.hasIncomingConnection()) {
-                            iter.remove();
-                        }
-                    }
-
-                    for (final Iterator<StandardRemoteGroupPort> iter = outputPorts.values().iterator(); iter.hasNext();) {
-                        final StandardRemoteGroupPort outputPort = iter.next();
-                        if (outputPort.getConnections().isEmpty()) {
-                            iter.remove();
-                        }
-                    }
-                } finally {
-                    writeLock.unlock();
-                }
-
                 throw new CommunicationsException("Unable to communicate with Remote NiFi at URI " + targetUris + " due to: " + e.getMessage());
             }
 
             writeLock.lock();
             try {
                 if (dto.getInputPorts() != null) {
-                    setInputPorts(convertRemotePort(dto.getInputPorts()));
+                    setInputPorts(convertRemotePort(dto.getInputPorts()), true);
                 }
                 if (dto.getOutputPorts() != null) {
-                    setOutputPorts(convertRemotePort(dto.getOutputPorts()));
+                    setOutputPorts(convertRemotePort(dto.getOutputPorts()), true);
                 }
 
                 // set the controller details

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
index ef69906..d006cff 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
@@ -347,7 +347,7 @@ public class MockProcessGroup implements ProcessGroup {
     }
 
     @Override
-    public ControllerServiceNode findControllerService(final String id) {
+    public ControllerServiceNode findControllerService(final String id, final boolean includeDescendants, final boolean includeAncestors) {
         return serviceMap.get(id);
     }
 
@@ -678,4 +678,8 @@ public class MockProcessGroup implements ProcessGroup {
     @Override
     public void verifyCanShowLocalModifications() {
     }
+
+    @Override
+    public void onComponentModified() {
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 98c7bc8..4945296 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -3731,7 +3731,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     private InstantiatedVersionedProcessGroup createFlowSnapshot(final String processGroupId) {
         final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId);
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, false);
+        final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, false);
         return versionedGroup;
     }
 
@@ -3753,21 +3753,39 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), false, NiFiUserUtils.getNiFiUser());
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, false);
+        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, false);
         final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents();
 
         final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localGroup);
         final ComparableDataFlow registryFlow = new StandardComparableDataFlow("Versioned Flow", registryGroup);
 
-        final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, new ConciseEvolvingDifferenceDescriptor());
+        final Set<String> ancestorServiceIds = getAncestorGroupServiceIds(processGroup);
+        final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, ancestorServiceIds, new ConciseEvolvingDifferenceDescriptor());
         final FlowComparison flowComparison = flowComparator.compare();
 
-        final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison);
+        final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison,
+            diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED && diff.getDifferenceType() != DifferenceType.STYLE_CHANGED);
+
         final FlowComparisonEntity entity = new FlowComparisonEntity();
         entity.setComponentDifferences(differenceDtos);
         return entity;
     }
 
+    private Set<String> getAncestorGroupServiceIds(final ProcessGroup group) {
+        final Set<String> ancestorServiceIds;
+        ProcessGroup parentGroup = group.getParent();
+
+        if (parentGroup == null) {
+            ancestorServiceIds = Collections.emptySet();
+        } else {
+            ancestorServiceIds = parentGroup.getControllerServices(true).stream()
+                .map(ControllerServiceNode::getIdentifier)
+                .collect(Collectors.toSet());
+        }
+
+        return ancestorServiceIds;
+    }
+
     @Override
     public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) throws IOException, NiFiRegistryException {
         final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
@@ -3852,12 +3870,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup localContents = mapper.mapProcessGroup(group, flowRegistryClient, true);
+        final VersionedProcessGroup localContents = mapper.mapProcessGroup(group, controllerFacade.getControllerServiceProvider(), flowRegistryClient, true);
 
         final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localContents);
         final ComparableDataFlow proposedFlow = new StandardComparableDataFlow("Versioned Flow", updatedSnapshot.getFlowContents());
 
-        final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow, new StaticDifferenceDescriptor());
+        final Set<String> ancestorGroupServiceIds = getAncestorGroupServiceIds(group);
+        final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow, ancestorGroupServiceIds, new StaticDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();
 
         final Set<AffectedComponentEntity> affectedComponents = comparison.getDifferences().stream()

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 3fa4462..7b753d6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -1717,7 +1717,7 @@ public class ProcessGroupResource extends ApplicationResource {
                         // To accomplish this, we call updateProcessGroupContents() passing 'true' for the updateSettings flag but null out the position.
                         flowSnapshot.getFlowContents().setPosition(null);
                         entity = serviceFacade.updateProcessGroupContents(NiFiUserUtils.getNiFiUser(), newGroupRevision, newGroupId,
-                        versionControlInfo, flowSnapshot, getIdGenerationSeed().orElse(null), false, true, true);
+                            versionControlInfo, flowSnapshot, getIdGenerationSeed().orElse(null), false, true, true);
                     }
 
                     populateRemainingProcessGroupEntityContent(entity);

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 5b33d90..8e5974a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -117,6 +117,8 @@ import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.VersionedFlowState;
+import org.apache.nifi.registry.flow.VersionedFlowStatus;
 import org.apache.nifi.registry.flow.diff.FlowComparison;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent;
@@ -213,6 +215,7 @@ import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -2187,17 +2190,23 @@ public final class DtoFactory {
 
 
     public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison) {
+        return createComponentDifferenceDtos(comparison, null);
+    }
+
+    public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison, final Predicate<FlowDifference> filter) {
         final Map<ComponentDifferenceDTO, List<DifferenceDTO>> differencesByComponent = new HashMap<>();
 
         for (final FlowDifference difference : comparison.getDifferences()) {
-            final ComponentDifferenceDTO componentDiff = createComponentDifference(difference);
-            final List<DifferenceDTO> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
+            if (filter == null || filter.test(difference)) {
+                final ComponentDifferenceDTO componentDiff = createComponentDifference(difference);
+                final List<DifferenceDTO> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
 
-            final DifferenceDTO dto = new DifferenceDTO();
-            dto.setDifferenceType(difference.getDifferenceType().getDescription());
-            dto.setDifference(difference.getDescription());
+                final DifferenceDTO dto = new DifferenceDTO();
+                dto.setDifferenceType(difference.getDifferenceType().getDescription());
+                dto.setDifference(difference.getDescription());
 
-            differences.add(dto);
+                differences.add(dto);
+            }
         }
 
         for (final Map.Entry<ComponentDifferenceDTO, List<DifferenceDTO>> entry : differencesByComponent.entrySet()) {
@@ -2252,6 +2261,12 @@ public final class DtoFactory {
         dto.setVersion(versionControlInfo.getVersion());
         dto.setCurrent(versionControlInfo.isCurrent());
         dto.setModified(versionControlInfo.isModified());
+
+        final VersionedFlowStatus status = versionControlInfo.getStatus();
+        final VersionedFlowState state = status.getState();
+        dto.setState(state == null ? null : state.name());
+        dto.setStateExplanation(status.getStateExplanation());
+
         return dto;
     }
 
@@ -3488,6 +3503,8 @@ public final class DtoFactory {
         copy.setVersion(original.getVersion());
         copy.setCurrent(original.getCurrent());
         copy.setModified(original.getModified());
+        copy.setState(original.getState());
+        copy.setStateExplanation(original.getStateExplanation());
         return copy;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
index 29e5f7d..907c8dc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
@@ -52,6 +52,7 @@ import org.apache.nifi.controller.queue.QueueSize;
 import org.apache.nifi.controller.repository.ContentNotFoundException;
 import org.apache.nifi.controller.repository.claim.ContentDirection;
 import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.controller.status.ConnectionStatus;
 import org.apache.nifi.controller.status.PortStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
@@ -172,6 +173,10 @@ public class ControllerFacade implements Authorizable {
         }
     }
 
+    public ControllerServiceProvider getControllerServiceProvider() {
+        return flowController;
+    }
+
     /**
      * Sets the name of this controller.
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
index 0f9ec7a..4d8e984 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.web.dao.impl;
 
+import static org.apache.nifi.controller.FlowController.ROOT_GROUP_ID_ALIAS;
+
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.components.state.Scope;
@@ -45,8 +47,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import static org.apache.nifi.controller.FlowController.ROOT_GROUP_ID_ALIAS;
-
 public class StandardControllerServiceDAO extends ComponentDAO implements ControllerServiceDAO {
 
     private ControllerServiceProvider serviceProvider;
@@ -172,6 +172,22 @@ public class StandardControllerServiceDAO extends ComponentDAO implements Contro
             }
         }
 
+        controllerService.getProcessGroup().onComponentModified();
+
+        // For any component that references this Controller Service, find the component's Process Group
+        // and notify the Process Group that a component has been modified. This way, we know to re-calculate
+        // whether or not the Process Group has local modifications.
+        final ProcessGroup group = controllerService.getProcessGroup();
+        controllerService.getReferences().getReferencingComponents().stream()
+            .map(ConfiguredComponent::getProcessGroupIdentifier)
+            .filter(id -> !id.equals(group.getIdentifier()))
+            .forEach(groupId -> {
+                final ProcessGroup descendant = group.findProcessGroup(groupId);
+                if (descendant != null) {
+                    descendant.onComponentModified();
+                }
+            });
+
         return controllerService;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardFunnelDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardFunnelDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardFunnelDAO.java
index e4ec239..60426c0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardFunnelDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardFunnelDAO.java
@@ -91,6 +91,8 @@ public class StandardFunnelDAO extends ComponentDAO implements FunnelDAO {
             }
         }
 
+        funnel.getProcessGroup().onComponentModified();
+
         return funnel;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
index f830e9b..2d47720 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
@@ -237,6 +237,7 @@ public class StandardInputPortDAO extends ComponentDAO implements PortDAO {
             inputPort.setMaxConcurrentTasks(concurrentTasks);
         }
 
+        inputPort.getProcessGroup().onComponentModified();
         return inputPort;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardLabelDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardLabelDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardLabelDAO.java
index 2a8b19f..b8105e6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardLabelDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardLabelDAO.java
@@ -102,6 +102,7 @@ public class StandardLabelDAO extends ComponentDAO implements LabelDAO {
             label.setSize(new Size(labelDTO.getWidth(), labelDTO.getHeight()));
         }
 
+        label.getProcessGroup().onComponentModified();
         return label;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardOutputPortDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardOutputPortDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardOutputPortDAO.java
index bad9e3a..72bc49b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardOutputPortDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardOutputPortDAO.java
@@ -233,6 +233,7 @@ public class StandardOutputPortDAO extends ComponentDAO implements PortDAO {
             outputPort.setMaxConcurrentTasks(concurrentTasks);
         }
 
+        outputPort.getProcessGroup().onComponentModified();
         return outputPort;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index bb7edb1..1aaf4cc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -204,7 +204,11 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
 
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
         for (final String serviceId : serviceIds) {
-            final ControllerServiceNode serviceNode = group.findControllerService(serviceId);
+            final ControllerServiceNode serviceNode = group.findControllerService(serviceId, true, true);
+            if (serviceNode == null) {
+                throw new ResourceNotFoundException("Could not find Controller Service with identifier " + serviceId);
+            }
+
             if (ControllerServiceState.ENABLED.equals(state)) {
                 final CompletableFuture<Void> serviceFuture = flowController.enableControllerService(serviceNode);
                 future = CompletableFuture.allOf(future, serviceFuture);
@@ -234,6 +238,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
             group.setComments(comments);
         }
 
+        group.onComponentModified();
         return group;
     }
 
@@ -247,7 +252,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         final String registryName = flowRegistry == null ? registryId : flowRegistry.getName();
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup flowSnapshot = mapper.mapProcessGroup(group, flowController.getFlowRegistryClient(), false);
+        final VersionedProcessGroup flowSnapshot = mapper.mapProcessGroup(group, flowController, flowController.getFlowRegistryClient(), false);
 
         final StandardVersionControlInformation vci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
             .registryName(registryName)
@@ -257,6 +262,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
             .build();
 
         group.setVersionControlInformation(vci, versionedComponentMapping);
+        group.onComponentModified();
 
         return group;
     }
@@ -279,6 +285,8 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
             .build();
 
         group.setVersionControlInformation(svci, Collections.emptyMap());
+        group.onComponentModified();
+
         return group;
     }
 
@@ -295,6 +303,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
             .forEach(var -> variableMap.put(var.getName(), var.getValue()));
 
         group.setVariables(variableMap);
+        group.onComponentModified();
         return group;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
index ffbe21c..95d0b54 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
@@ -411,6 +411,7 @@ public class StandardProcessorDAO extends ComponentDAO implements ProcessorDAO {
 
         // configure the processor
         configureProcessor(processor, processorDTO);
+        parentGroup.onComponentModified();
 
         // attempt to change the underlying processor if an updated bundle is specified
         // updating the bundle must happen after configuring so that any additional classpath resources are set first

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
index d638839..c570dfc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardRemoteProcessGroupDAO.java
@@ -314,6 +314,7 @@ public class StandardRemoteProcessGroupDAO extends ComponentDAO implements Remot
         // perform the update
         updatePort(port, remoteProcessGroupPortDto, remoteProcessGroup);
 
+        remoteProcessGroup.getProcessGroup().onComponentModified();
         return port;
     }
 
@@ -332,6 +333,7 @@ public class StandardRemoteProcessGroupDAO extends ComponentDAO implements Remot
 
         // perform the update
         updatePort(port, remoteProcessGroupPortDto, remoteProcessGroup);
+        remoteProcessGroup.getProcessGroup().onComponentModified();
 
         return port;
     }
@@ -373,8 +375,6 @@ public class StandardRemoteProcessGroupDAO extends ComponentDAO implements Remot
     public RemoteProcessGroup updateRemoteProcessGroup(RemoteProcessGroupDTO remoteProcessGroupDTO) {
         RemoteProcessGroup remoteProcessGroup = locateRemoteProcessGroup(remoteProcessGroupDTO.getId());
         return updateRemoteProcessGroup(remoteProcessGroup, remoteProcessGroupDTO);
-
-
     }
 
     private RemoteProcessGroup updateRemoteProcessGroup(RemoteProcessGroup remoteProcessGroup, RemoteProcessGroupDTO remoteProcessGroupDTO) {
@@ -447,6 +447,7 @@ public class StandardRemoteProcessGroupDAO extends ComponentDAO implements Remot
             }
         }
 
+        remoteProcessGroup.getProcessGroup().onComponentModified();
         return remoteProcessGroup;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
index 7fdaf56..a801dcb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
@@ -26,6 +26,7 @@ import org.apache.nifi.web.api.dto.DtoFactory;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupContentsDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 
@@ -38,6 +39,9 @@ public class AffectedComponentUtils {
             case AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR:
                 final ProcessorEntity procEntity = serviceFacade.getProcessor(componentEntity.getId(), user);
                 return dtoFactory.createAffectedComponentEntity(procEntity);
+            case AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE:
+                final ControllerServiceEntity serviceEntity = serviceFacade.getControllerService(componentEntity.getId(), user);
+                return dtoFactory.createAffectedComponentEntity(serviceEntity);
             case AffectedComponentDTO.COMPONENT_TYPE_REMOTE_INPUT_PORT: {
                 final RemoteProcessGroupEntity remoteGroupEntity = serviceFacade.getRemoteProcessGroup(componentEntity.getComponent().getProcessGroupId(), user);
                 final RemoteProcessGroupContentsDTO remoteGroupContents = remoteGroupEntity.getComponent().getContents();

http://git-wip-us.apache.org/repos/asf/nifi/blob/fdef5b56/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
index 3961be7..28fef0f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
@@ -107,7 +107,8 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
 
             final int scheduleComponentStatus = clusterResponse.getStatus();
             if (scheduleComponentStatus != Status.OK.getStatusCode()) {
-                throw new LifecycleManagementException("Failed to transition components to a state of " + desiredState);
+                final String explanation = getResponseEntity(clusterResponse, String.class);
+                throw new LifecycleManagementException("Failed to transition components to a state of " + desiredState + " due to " + explanation);
             }
 
             final boolean processorsTransitioned = waitForProcessorStatus(user, exampleUri, groupId, componentMap, desiredState, pause);
@@ -312,7 +313,8 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
 
             final int disableServicesStatus = clusterResponse.getStatus();
             if (disableServicesStatus != Status.OK.getStatusCode()) {
-                throw new LifecycleManagementException("Failed to update Controller Services to a state of " + desiredState);
+                final String explanation = getResponseEntity(clusterResponse, String.class);
+                throw new LifecycleManagementException("Failed to update Controller Services to a state of " + desiredState + " due to " + explanation);
             }
 
             final boolean serviceTransitioned = waitForControllerServiceStatus(user, originalUri, groupId, affectedServiceIds, desiredState, pause);


[21/50] nifi git commit: NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated autho

Posted by bb...@apache.org.
NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated authorizations required for version control endpoints - Add state and percent complete fields ot VersionedFlowUpdateRequestDTO - If a variable exists in a parent process group, do not include it in imported/updated process group when interacting with flow registry - Code cleanup, documentation; bug fixes - Ensure that we are passing NiFiUser to the flow registry client when appropriate - Updated to work against new version of flow registry client; deleted file-based flow registry client

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/f6cc5b6c
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/f6cc5b6c
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/f6cc5b6c

Branch: refs/heads/master
Commit: f6cc5b6cdca86b3f535754f7ec4d91ca61f2e35b
Parents: d6e54f1
Author: Mark Payne <ma...@hotmail.com>
Authored: Sat Nov 4 14:19:49 2017 -0400
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:53 2018 -0500

----------------------------------------------------------------------
 .../apache/nifi/registry/flow/FlowRegistry.java |  54 +-
 .../flow/VersionControlInformation.java         |  14 +-
 .../nifi/groups/StandardProcessGroup.java       |  53 +-
 .../registry/flow/FileBasedFlowRegistry.java    | 509 -------------------
 .../registry/flow/RestBasedFlowRegistry.java    |  64 ++-
 .../flow/StandardFlowRegistryClient.java        |  14 +-
 .../flow/StandardVersionControlInformation.java |  23 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  10 +
 .../nifi/web/StandardNiFiServiceFacade.java     |  51 +-
 .../nifi/web/api/ProcessGroupResource.java      |  12 +-
 .../apache/nifi/web/api/VersionsResource.java   | 361 +++++++------
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  57 ++-
 12 files changed, 424 insertions(+), 798 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
index 10db9cf..76f96f2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -84,14 +84,6 @@ public interface FlowRegistry {
     Bucket getBucket(String bucketId, NiFiUser user) throws IOException, NiFiRegistryException;
 
     /**
-     * Gets the bucket with the given ID
-     *
-     * @param bucketId the id of the bucket
-     * @return the bucket with the given ID
-     */
-    Bucket getBucket(String bucketId) throws IOException, NiFiRegistryException;
-
-    /**
      * Retrieves the set of all Versioned Flows for the specified bucket
      *
      * @param bucketId the ID of the bucket
@@ -123,7 +115,15 @@ public interface FlowRegistry {
      * @throws NullPointerException if the VersionedFlow is null, or if its bucket identifier or name is null
      * @throws NiFiRegistryException if the bucket id does not exist
      */
-    VersionedFlow registerVersionedFlow(VersionedFlow flow) throws IOException, NiFiRegistryException;
+    VersionedFlow registerVersionedFlow(VersionedFlow flow, NiFiUser user) throws IOException, NiFiRegistryException;
+
+    /**
+     * Deletes the specified flow from the Flow Registry
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     */
+    VersionedFlow deleteVersionedFlow(String bucketId, String flowId, NiFiUser user) throws IOException, NiFiRegistryException;
 
     /**
      * Adds the given snapshot to the Flow Registry for the given flow
@@ -138,7 +138,8 @@ public interface FlowRegistry {
      * @throws NullPointerException if the VersionedFlow is null, or if its bucket identifier is null, or if the flow to snapshot is null
      * @throws NiFiRegistryException if the flow does not exist
      */
-    VersionedFlowSnapshot registerVersionedFlowSnapshot(VersionedFlow flow, VersionedProcessGroup snapshot, String comments, int expectedVersion) throws IOException, NiFiRegistryException;
+    VersionedFlowSnapshot registerVersionedFlowSnapshot(VersionedFlow flow, VersionedProcessGroup snapshot, String comments, int expectedVersion, NiFiUser user)
+        throws IOException, NiFiRegistryException;
 
     /**
      * Returns the latest (most recent) version of the Flow in the Flow Registry for the given bucket and flow
@@ -150,7 +151,23 @@ public interface FlowRegistry {
      * @throws IOException if unable to communicate with the Flow Registry
      * @throws NiFiRegistryException if unable to find the bucket with the given ID or the flow with the given ID
      */
-    int getLatestVersion(String bucketId, String flowId) throws IOException, NiFiRegistryException;
+    int getLatestVersion(String bucketId, String flowId, NiFiUser user) throws IOException, NiFiRegistryException;
+
+    /**
+     * Retrieves the contents of the Flow with the given Bucket ID, Flow ID, and version, from the Flow Registry
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @param version the version to retrieve
+     * @return the contents of the Flow from the Flow Registry
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws NiFiRegistryException if unable to find the contents of the flow due to the bucket or flow not existing,
+     *             or the specified version of the flow not existing
+     * @throws NullPointerException if any of the arguments is not specified
+     * @throws IllegalArgumentException if the given version is less than 1
+     */
+    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version, NiFiUser user) throws IOException, NiFiRegistryException;
 
     /**
      * Retrieves the contents of the Flow with the given Bucket ID, Flow ID, and version, from the Flow Registry
@@ -166,7 +183,6 @@ public interface FlowRegistry {
      * @throws NullPointerException if any of the arguments is not specified
      * @throws IllegalArgumentException if the given version is less than 1
      */
-    // TODO: Need to pass in user
     VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version) throws IOException, NiFiRegistryException;
 
     /**
@@ -179,6 +195,18 @@ public interface FlowRegistry {
      * @throws IOException if unable to communicate with the Flow Registry
      * @throws NiFiRegistryException if unable to find a flow with the given bucket ID and flow ID
      */
-    // TODO: Need to pass in user
+    VersionedFlow getVersionedFlow(String bucketId, String flowId, NiFiUser user) throws IOException, NiFiRegistryException;
+
+    /**
+     * Retrieves a VersionedFlow by bucket id and flow id
+     *
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @return the VersionedFlow for the given bucket and flow ID's
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     * @throws NiFiRegistryException if unable to find a flow with the given bucket ID and flow ID
+     */
+    // TODO: Do we still need this?
     VersionedFlow getVersionedFlow(String bucketId, String flowId) throws IOException, NiFiRegistryException;
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
index 67c3635..b54a1c9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionControlInformation.java
@@ -17,8 +17,6 @@
 
 package org.apache.nifi.registry.flow;
 
-import java.util.Optional;
-
 /**
  * <p>
  * Provides a mechanism for conveying which Flow Registry a flow is stored in, and
@@ -69,18 +67,14 @@ public interface VersionControlInformation {
 
     /**
      * @return <code>true</code> if the flow has been modified since the last time that it was updated from the Flow Registry or saved
-     *         to the Flow Registry; <code>false</code> if the flow is in sync with the Flow Registry. An empty optional will be returned
-     *         if it is not yet known whether or not the flow has been modified (for example, on startup, when the flow has not yet been
-     *         fetched from the Flow Registry)
+     *         to the Flow Registry; <code>false</code> if the flow is in sync with the Flow Registry.
      */
-    Optional<Boolean> getModified();
+    boolean isModified();
 
     /**
-     * @return <code>true</code> if this version of the flow is the most recent version of the flow available in the Flow Registry.
-     *         An empty optional will be returned if it is not yet known whether or not the flow has been modified (for example, on startup,
-     *         when the flow has not yet been fetched from the Flow Registry)
+     * @return <code>true</code> if this version of the flow is the most recent version of the flow available in the Flow Registry, <code>false</code> otherwise.
      */
-    Optional<Boolean> getCurrent();
+    boolean isCurrent();
 
     /**
      * @return the snapshot of the flow that was synchronized with the Flow Registry

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 2783e96..d1aa4e2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -2822,17 +2822,17 @@ public final class StandardProcessGroup implements ProcessGroup {
             versionControlInformation.getFlowIdentifier(),
             versionControlInformation.getVersion(),
             versionControlInformation.getFlowSnapshot(),
-            versionControlInformation.getModified().orElse(null),
-            versionControlInformation.getCurrent().orElse(null)) {
+            versionControlInformation.isModified(),
+            versionControlInformation.isCurrent()) {
 
             @Override
-            public Optional<Boolean> getModified() {
+            public boolean isModified() {
                 final Set<FlowDifference> differences = StandardProcessGroup.this.getModifications();
                 if (differences == null) {
-                    return Optional.ofNullable(null);
+                    return false;
                 }
 
-                return Optional.of(!differences.isEmpty());
+                return !differences.isEmpty();
             }
         };
 
@@ -2938,7 +2938,6 @@ public final class StandardProcessGroup implements ProcessGroup {
         try {
             final VersionedFlow versionedFlow = flowRegistry.getVersionedFlow(vci.getBucketIdentifier(), vci.getFlowIdentifier());
             final int latestVersion = (int) versionedFlow.getVersionCount();
-
             vci.setBucketName(versionedFlow.getBucketName());
             vci.setFlowName(versionedFlow.getName());
             vci.setFlowDescription(versionedFlow.getDescription());
@@ -2986,7 +2985,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 LOG.info("Updating {} to {}; there are {} differences to take into account:\n{}", this, proposedSnapshot, flowComparison.getDifferences().size(), differencesByLine);
             }
 
-            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings);
+            final Set<String> knownVariables = getKnownVariableNames();
+            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings, knownVariables);
         } catch (final ProcessorInstantiationException pie) {
             throw new RuntimeException(pie);
         } finally {
@@ -2994,9 +2994,26 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    private Set<String> getKnownVariableNames() {
+        final Set<String> variableNames = new HashSet<>();
+        populateKnownVariableNames(this, variableNames);
+        return variableNames;
+    }
+
+    private void populateKnownVariableNames(final ProcessGroup group, final Set<String> knownVariables) {
+        group.getVariableRegistry().getVariableMap().keySet().stream()
+            .map(VariableDescriptor::getName)
+            .forEach(knownVariables::add);
+
+        final ProcessGroup parent = group.getParent();
+        if (parent != null) {
+            populateKnownVariableNames(parent, knownVariables);
+        }
+    }
+
 
     private void updateProcessGroup(final ProcessGroup group, final VersionedProcessGroup proposed, final String componentIdSeed,
-        final Set<String> updatedVersionedComponentIds, final boolean updatePosition, final boolean updateName) throws ProcessorInstantiationException {
+        final Set<String> updatedVersionedComponentIds, final boolean updatePosition, final boolean updateName, final Set<String> variablesToSkip) throws ProcessorInstantiationException {
 
         group.setComments(proposed.getComments());
 
@@ -3027,7 +3044,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
         // If any new variables exist in the proposed flow, add those to the variable registry.
         for (final Map.Entry<String, String> entry : proposed.getVariables().entrySet()) {
-            if (!existingVariableNames.contains(entry.getKey())) {
+            if (!existingVariableNames.contains(entry.getKey()) && !variablesToSkip.contains(entry.getKey())) {
                 updatedVariableMap.put(entry.getKey(), entry.getValue());
             }
         }
@@ -3068,10 +3085,10 @@ public final class StandardProcessGroup implements ProcessGroup {
             final ProcessGroup childGroup = childGroupsByVersionedId.get(proposedChildGroup.getIdentifier());
 
             if (childGroup == null) {
-                final ProcessGroup added = addProcessGroup(group, proposedChildGroup, componentIdSeed);
+                final ProcessGroup added = addProcessGroup(group, proposedChildGroup, componentIdSeed, variablesToSkip);
                 LOG.info("Added {} to {}", added, this);
             } else {
-                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName);
+                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName, variablesToSkip);
                 LOG.info("Updated {}", childGroup);
             }
 
@@ -3345,11 +3362,12 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
 
-    private ProcessGroup addProcessGroup(final ProcessGroup destination, final VersionedProcessGroup proposed, final String componentIdSeed) throws ProcessorInstantiationException {
+    private ProcessGroup addProcessGroup(final ProcessGroup destination, final VersionedProcessGroup proposed, final String componentIdSeed, final Set<String> variablesToSkip)
+            throws ProcessorInstantiationException {
         final ProcessGroup group = flowController.createProcessGroup(generateUuid(componentIdSeed));
         group.setVersionedComponentId(proposed.getIdentifier());
         group.setParent(destination);
-        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true);
+        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true, variablesToSkip);
         destination.addProcessGroup(group);
         return group;
     }
@@ -3771,16 +3789,11 @@ public final class StandardProcessGroup implements ProcessGroup {
                 }
 
                 if (verifyNotDirty) {
-                    final Optional<Boolean> modifiedOption = versionControlInfo.getModified();
-                    if (!modifiedOption.isPresent()) {
-                        throw new IllegalStateException(this + " cannot be updated to a different version of the flow because the local flow "
-                            + "has not yet been synchronized with the Flow Registry. The Process Group must be"
-                            + " synched with the Flow Registry before continuing. This will happen periodically in the background, so please try the request again later");
-                    }
+                    final boolean modified = versionControlInfo.isModified();
 
                     final Set<FlowDifference> modifications = getModifications();
 
-                    if (Boolean.TRUE.equals(modifiedOption.get())) {
+                    if (modified) {
                         final String changes = modifications.stream()
                             .map(FlowDifference::toString)
                             .collect(Collectors.joining("\n"));

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
deleted file mode 100644
index 9b3ba94..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/FileBasedFlowRegistry.java
+++ /dev/null
@@ -1,509 +0,0 @@
-/*
- * 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.registry.flow;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.UUID;
-
-import org.apache.nifi.authorization.user.NiFiUser;
-import org.apache.nifi.registry.bucket.Bucket;
-import org.apache.nifi.registry.client.NiFiRegistryException;
-
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * A simple file-based implementation of a Flow Registry Client. Rather than interacting
- * with an actual Flow Registry, this implementation simply reads flows from disk and writes
- * them to disk. It is not meant for any production use but is available for testing purposes.
- */
-public class FileBasedFlowRegistry implements FlowRegistry {
-    private final File directory;
-    private final Map<String, Set<String>> flowNamesByBucket = new HashMap<>();
-    private final JsonFactory jsonFactory = new JsonFactory();
-    private final String id;
-    private volatile String name = "Local Registry";
-    private volatile String url = "file:" + (new File("..").getAbsolutePath());
-    private volatile String description = "Default file-based Flow Registry";
-
-    public FileBasedFlowRegistry(final String id, final String url) throws IOException {
-        final URI uri = URI.create(url);
-        if (!uri.getScheme().equalsIgnoreCase("file")) {
-            throw new IllegalArgumentException("Cannot create a File Based Flow Registry with a URL of " + url + "; URL scheme must be 'file'");
-        }
-
-        this.directory = new File(URI.create(url).getPath());
-
-        if (!directory.exists() && !directory.mkdirs()) {
-            throw new IOException("Could not access or create directory " + directory.getAbsolutePath() + " for Flow Registry");
-        }
-
-        this.id = id;
-        this.url = url;
-        recoverBuckets();
-    }
-
-    private void recoverBuckets() throws IOException {
-        final File[] bucketDirs = directory.listFiles();
-        if (bucketDirs == null) {
-            throw new IOException("Could not get listing of directory " + directory);
-        }
-
-        for (final File bucketDir : bucketDirs) {
-            final File[] flowDirs = bucketDir.listFiles();
-            if (flowDirs == null) {
-                throw new IOException("Could not get listing of directory " + bucketDir);
-            }
-
-            final Set<String> flowNames = new HashSet<>();
-            for (final File flowDir : flowDirs) {
-                final File propsFile = new File(flowDir, "flow.properties");
-                if (!propsFile.exists()) {
-                    continue;
-                }
-
-                final Properties properties = new Properties();
-                try (final InputStream in = new FileInputStream(propsFile)) {
-                    properties.load(in);
-                }
-
-                final String flowName = properties.getProperty("name");
-                if (flowName == null) {
-                    continue;
-                }
-
-                flowNames.add(flowName);
-            }
-
-            if (!flowNames.isEmpty()) {
-                flowNamesByBucket.put(bucketDir.getName(), flowNames);
-            }
-        }
-    }
-
-    @Override
-    public String getURL() {
-        return url;
-    }
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    @Override
-    public Set<Bucket> getBuckets(NiFiUser user) throws IOException {
-        final Set<Bucket> buckets = new HashSet<>();
-
-        final File[] bucketDirs = directory.listFiles();
-        if (bucketDirs == null) {
-            throw new IOException("Could not get listing of directory " + directory);
-        }
-
-        for (final File bucketDirectory : bucketDirs) {
-            final String bucketIdentifier = bucketDirectory.getName();
-            final long creation = bucketDirectory.lastModified();
-
-            final Bucket bucket = new Bucket();
-            bucket.setIdentifier(bucketIdentifier);
-            bucket.setName("Bucket '" + bucketIdentifier + "'");
-            bucket.setCreatedTimestamp(creation);
-
-            final Set<VersionedFlow> versionedFlows = new HashSet<>();
-            final File[] flowDirs = bucketDirectory.listFiles();
-            if (flowDirs != null) {
-                for (final File flowDir : flowDirs) {
-                    final String flowIdentifier = flowDir.getName();
-                    try {
-                        final VersionedFlow versionedFlow = getVersionedFlow(bucketIdentifier, flowIdentifier);
-                        versionedFlows.add(versionedFlow);
-                    } catch (NiFiRegistryException e) {
-                        continue;
-                    }
-                }
-            }
-
-            bucket.setVersionedFlows(versionedFlows);
-
-            buckets.add(bucket);
-        }
-
-        return buckets;
-    }
-
-    @Override
-    public Bucket getBucket(String bucketId) throws IOException, NiFiRegistryException {
-        return getBucket(bucketId, null);
-    }
-
-    @Override
-    public Bucket getBucket(String bucketId, NiFiUser user) throws IOException, NiFiRegistryException {
-        return getBuckets(user).stream().filter(b -> b.getIdentifier().equals(bucketId)).findFirst().orElse(null);
-    }
-
-    @Override
-    public Set<VersionedFlow> getFlows(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final Bucket bucket = getBuckets(user).stream().filter(b -> bucketId.equals(b.getIdentifier())).findFirst().orElse(null);
-        if (bucket == null) {
-            return Collections.emptySet();
-        }
-
-        return bucket.getVersionedFlows();
-    }
-
-    @Override
-    public Set<VersionedFlowSnapshotMetadata> getFlowVersions(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final VersionedFlow flow = getFlows(bucketId, user).stream().filter(f -> flowId.equals(f.getIdentifier())).findFirst().orElse(null);
-        if (flow == null) {
-            return Collections.emptySet();
-        }
-
-        return flow.getSnapshotMetadata();
-    }
-
-    @Override
-    public synchronized VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, NiFiRegistryException {
-        Objects.requireNonNull(flow);
-        Objects.requireNonNull(flow.getBucketIdentifier());
-        Objects.requireNonNull(flow.getName());
-
-        // Verify that bucket exists
-        final File bucketDir = new File(directory, flow.getBucketIdentifier());
-        if (!bucketDir.exists()) {
-            throw new NiFiRegistryException("No bucket exists with ID " + flow.getBucketIdentifier());
-        }
-
-        // Verify that there is no flow with the same name in that bucket
-        final Set<String> flowNames = flowNamesByBucket.get(flow.getBucketIdentifier());
-        if (flowNames != null && flowNames.contains(flow.getName())) {
-            throw new IllegalArgumentException("Flow with name '" + flow.getName() + "' already exists for Bucket with ID " + flow.getBucketIdentifier());
-        }
-
-        final String flowIdentifier = UUID.randomUUID().toString();
-        final File flowDir = new File(bucketDir, flowIdentifier);
-        if (!flowDir.mkdirs()) {
-            throw new IOException("Failed to create directory " + flowDir + " for new Flow");
-        }
-
-        final File propertiesFile = new File(flowDir, "flow.properties");
-
-        final Properties flowProperties = new Properties();
-        flowProperties.setProperty("name", flow.getName());
-        flowProperties.setProperty("created", String.valueOf(flow.getCreatedTimestamp()));
-        flowProperties.setProperty("description", flow.getDescription());
-        flowProperties.setProperty("lastModified", String.valueOf(flow.getModifiedTimestamp()));
-
-        try (final OutputStream out = new FileOutputStream(propertiesFile)) {
-            flowProperties.store(out, null);
-        }
-
-        final VersionedFlow response = new VersionedFlow();
-        response.setBucketIdentifier(flow.getBucketIdentifier());
-        response.setCreatedTimestamp(flow.getCreatedTimestamp());
-        response.setDescription(flow.getDescription());
-        response.setIdentifier(flowIdentifier);
-        response.setModifiedTimestamp(flow.getModifiedTimestamp());
-        response.setName(flow.getName());
-
-        return response;
-    }
-
-    @Override
-    public synchronized VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments, final int expectedVersion)
-        throws IOException, NiFiRegistryException {
-        Objects.requireNonNull(flow);
-        Objects.requireNonNull(flow.getBucketIdentifier());
-        Objects.requireNonNull(flow.getName());
-        Objects.requireNonNull(snapshot);
-
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, flow.getBucketIdentifier());
-        if (!bucketDir.exists()) {
-            throw new NiFiRegistryException("No bucket exists with ID " + flow.getBucketIdentifier());
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flow.getIdentifier());
-        if (!flowDir.exists()) {
-            throw new NiFiRegistryException("No Flow with ID " + flow.getIdentifier() + " exists for Bucket with ID " + flow.getBucketIdentifier());
-        }
-
-        final File[] versionDirs = flowDir.listFiles();
-        if (versionDirs == null) {
-            throw new IOException("Unable to perform listing of directory " + flowDir);
-        }
-
-        int maxVersion = 0;
-        for (final File versionDir : versionDirs) {
-            final String versionName = versionDir.getName();
-
-            final int version;
-            try {
-                version = Integer.parseInt(versionName);
-            } catch (final NumberFormatException nfe) {
-                continue;
-            }
-
-            if (version > maxVersion) {
-                maxVersion = version;
-            }
-        }
-
-        final int snapshotVersion = maxVersion + 1;
-        final File snapshotDir = new File(flowDir, String.valueOf(snapshotVersion));
-        if (!snapshotDir.mkdir()) {
-            throw new IOException("Could not create directory " + snapshotDir);
-        }
-
-        final File contentsFile = new File(snapshotDir, "flow.xml");
-
-        try (final OutputStream out = new FileOutputStream(contentsFile);
-            final JsonGenerator generator = jsonFactory.createGenerator(out)) {
-            generator.setCodec(new ObjectMapper());
-            generator.setPrettyPrinter(new DefaultPrettyPrinter());
-            generator.writeObject(snapshot);
-        }
-
-        final Properties snapshotProperties = new Properties();
-        snapshotProperties.setProperty("comments", comments);
-        snapshotProperties.setProperty("name", flow.getName());
-        final File snapshotPropsFile = new File(snapshotDir, "snapshot.properties");
-        try (final OutputStream out = new FileOutputStream(snapshotPropsFile)) {
-            snapshotProperties.store(out, null);
-        }
-
-        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
-        snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier());
-        snapshotMetadata.setComments(comments);
-        snapshotMetadata.setFlowIdentifier(flow.getIdentifier());
-        snapshotMetadata.setFlowName(flow.getName());
-        snapshotMetadata.setTimestamp(System.currentTimeMillis());
-        snapshotMetadata.setVersion(snapshotVersion);
-
-        final VersionedFlowSnapshot response = new VersionedFlowSnapshot();
-        response.setSnapshotMetadata(snapshotMetadata);
-        response.setFlowContents(snapshot);
-        return response;
-    }
-
-    @Override
-    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, bucketId);
-        if (!bucketDir.exists()) {
-            throw new NiFiRegistryException("No bucket exists with ID " + bucketId);
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flowId);
-        if (!flowDir.exists()) {
-            throw new NiFiRegistryException("No Flow with ID " + flowId + " exists for Bucket with ID " + bucketId);
-        }
-
-        final File[] versionDirs = flowDir.listFiles();
-        if (versionDirs == null) {
-            throw new IOException("Unable to perform listing of directory " + flowDir);
-        }
-
-        int maxVersion = 0;
-        for (final File versionDir : versionDirs) {
-            final String versionName = versionDir.getName();
-
-            final int version;
-            try {
-                version = Integer.parseInt(versionName);
-            } catch (final NumberFormatException nfe) {
-                continue;
-            }
-
-            if (version > maxVersion) {
-                maxVersion = version;
-            }
-        }
-
-        return maxVersion;
-    }
-
-    @Override
-    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, int version) throws IOException, NiFiRegistryException {
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, bucketId);
-        if (!bucketDir.exists()) {
-            throw new NiFiRegistryException("No bucket exists with ID " + bucketId);
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flowId);
-        if (!flowDir.exists()) {
-            throw new NiFiRegistryException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
-        }
-
-        final File versionDir = new File(flowDir, String.valueOf(version));
-        if (!versionDir.exists()) {
-            throw new NiFiRegistryException("Flow with ID " + flowId + " in Bucket with ID " + bucketId + " does not contain a snapshot with version " + version);
-        }
-
-        final File contentsFile = new File(versionDir, "flow.xml");
-
-        final VersionedProcessGroup processGroup;
-        try (final JsonParser parser = jsonFactory.createParser(contentsFile)) {
-            final ObjectMapper mapper = new ObjectMapper();
-            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
-            parser.setCodec(mapper);
-            processGroup = parser.readValueAs(VersionedProcessGroup.class);
-        }
-
-        final Properties properties = new Properties();
-        final File snapshotPropsFile = new File(versionDir, "snapshot.properties");
-        try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
-            properties.load(in);
-        }
-
-        final String comments = properties.getProperty("comments");
-        final String flowName = properties.getProperty("name");
-
-        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
-        snapshotMetadata.setBucketIdentifier(bucketId);
-        snapshotMetadata.setComments(comments);
-        snapshotMetadata.setFlowIdentifier(flowId);
-        snapshotMetadata.setFlowName(flowName);
-        snapshotMetadata.setTimestamp(System.currentTimeMillis());
-        snapshotMetadata.setVersion(version);
-
-        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
-        snapshot.setFlowContents(processGroup);
-        snapshot.setSnapshotMetadata(snapshotMetadata);
-
-        return snapshot;
-    }
-
-    @Override
-    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
-        // Verify that the bucket exists
-        final File bucketDir = new File(directory, bucketId);
-        if (!bucketDir.exists()) {
-            throw new NiFiRegistryException("No bucket exists with ID " + bucketId);
-        }
-
-        // Verify that the flow exists
-        final File flowDir = new File(bucketDir, flowId);
-        if (!flowDir.exists()) {
-            throw new NiFiRegistryException("No Flow with ID " + flowId + " exists for Bucket with ID " + flowId);
-        }
-
-        final File flowPropsFile = new File(flowDir, "flow.properties");
-        final Properties flowProperties = new Properties();
-        try (final InputStream in = new FileInputStream(flowPropsFile)) {
-            flowProperties.load(in);
-        }
-
-        final VersionedFlow flow = new VersionedFlow();
-        flow.setBucketIdentifier(bucketId);
-        flow.setCreatedTimestamp(Long.parseLong(flowProperties.getProperty("created")));
-        flow.setDescription(flowProperties.getProperty("description"));
-        flow.setIdentifier(flowId);
-        flow.setModifiedTimestamp(flowDir.lastModified());
-        flow.setName(flowProperties.getProperty("name"));
-
-        final Comparator<VersionedFlowSnapshotMetadata> versionComparator = (a, b) -> Integer.compare(a.getVersion(), b.getVersion());
-
-        final SortedSet<VersionedFlowSnapshotMetadata> snapshotMetadataSet = new TreeSet<>(versionComparator);
-        flow.setSnapshotMetadata(snapshotMetadataSet);
-
-        final File[] versionDirs = flowDir.listFiles();
-        flow.setVersionCount(versionDirs.length);
-
-        for (final File file : versionDirs) {
-            if (!file.isDirectory()) {
-                continue;
-            }
-
-            int version;
-            try {
-                version = Integer.parseInt(file.getName());
-            } catch (final NumberFormatException nfe) {
-                // not a version. skip.
-                continue;
-            }
-
-            final File snapshotPropsFile = new File(file, "snapshot.properties");
-            final Properties snapshotProperties = new Properties();
-            try (final InputStream in = new FileInputStream(snapshotPropsFile)) {
-                snapshotProperties.load(in);
-            }
-
-            final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
-            metadata.setBucketIdentifier(bucketId);
-            metadata.setComments(snapshotProperties.getProperty("comments"));
-            metadata.setFlowIdentifier(flowId);
-            metadata.setFlowName(snapshotProperties.getProperty("name"));
-            metadata.setTimestamp(file.lastModified());
-            metadata.setVersion(version);
-
-            snapshotMetadataSet.add(metadata);
-        }
-
-        return flow;
-    }
-
-    @Override
-    public String getIdentifier() {
-        return id;
-    }
-
-    @Override
-    public String getDescription() {
-        return description;
-    }
-
-    @Override
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    @Override
-    public void setURL(String url) {
-        this.url = url;
-    }
-
-    @Override
-    public void setName(String name) {
-        this.name = name;
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
index 26be69b..8bf89c6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
@@ -111,55 +111,59 @@ public class RestBasedFlowRegistry implements FlowRegistry {
         this.name = name;
     }
 
-    @Override
-    public Set<Bucket> getBuckets(final NiFiUser user) throws IOException, NiFiRegistryException {
-        final BucketClient bucketClient = getRegistryClient().getBucketClient(user.isAnonymous() ? null : user.getIdentity());
-        return new HashSet<>(bucketClient.getAll());
+    private String getIdentity(final NiFiUser user) {
+        return (user == null || user.isAnonymous()) ? null : user.getIdentity();
     }
 
     @Override
-    public Bucket getBucket(final String bucketId) throws IOException, NiFiRegistryException {
-        final BucketClient bucketClient = getRegistryClient().getBucketClient();
-        return bucketClient.get(bucketId);
+    public Set<Bucket> getBuckets(final NiFiUser user) throws IOException, NiFiRegistryException {
+        final BucketClient bucketClient = getRegistryClient().getBucketClient(getIdentity(user));
+        return new HashSet<>(bucketClient.getAll());
     }
 
     @Override
     public Bucket getBucket(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final BucketClient bucketClient = getRegistryClient().getBucketClient(user.isAnonymous() ? null : user.getIdentity());
+        final BucketClient bucketClient = getRegistryClient().getBucketClient(getIdentity(user));
         return bucketClient.get(bucketId);
     }
 
 
     @Override
     public Set<VersionedFlow> getFlows(final String bucketId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowClient flowClient = getRegistryClient().getFlowClient(user.isAnonymous() ? null : user.getIdentity());
+        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
         return new HashSet<>(flowClient.getByBucket(bucketId));
     }
 
     @Override
     public Set<VersionedFlowSnapshotMetadata> getFlowVersions(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
-        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(user.isAnonymous() ? null : user.getIdentity());
+        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
         return new HashSet<>(snapshotClient.getSnapshotMetadata(bucketId, flowId));
     }
 
     @Override
-    public VersionedFlow registerVersionedFlow(final VersionedFlow flow) throws IOException, NiFiRegistryException {
-        final FlowClient flowClient = getRegistryClient().getFlowClient();
+    public VersionedFlow registerVersionedFlow(final VersionedFlow flow, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
         return flowClient.create(flow);
     }
 
     @Override
-    public VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot, final String comments, final int expectedVersion)
-            throws IOException, NiFiRegistryException {
+    public VersionedFlow deleteVersionedFlow(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
+        return flowClient.delete(bucketId, flowId);
+    }
+
+    @Override
+    public VersionedFlowSnapshot registerVersionedFlowSnapshot(final VersionedFlow flow, final VersionedProcessGroup snapshot,
+        final String comments, final int expectedVersion, final NiFiUser user) throws IOException, NiFiRegistryException {
 
-        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient();
+        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
         final VersionedFlowSnapshot versionedFlowSnapshot = new VersionedFlowSnapshot();
         versionedFlowSnapshot.setFlowContents(snapshot);
 
         final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
         metadata.setBucketIdentifier(flow.getBucketIdentifier());
         metadata.setFlowIdentifier(flow.getIdentifier());
-        metadata.setFlowName(flow.getName());
+        metadata.setAuthor(getIdentity(user));
         metadata.setTimestamp(System.currentTimeMillis());
         metadata.setVersion(expectedVersion);
         metadata.setComments(comments);
@@ -169,24 +173,29 @@ public class RestBasedFlowRegistry implements FlowRegistry {
     }
 
     @Override
-    public int getLatestVersion(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
-        return (int) getRegistryClient().getFlowClient().get(bucketId, flowId).getVersionCount();
+    public int getLatestVersion(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        return (int) getRegistryClient().getFlowClient(getIdentity(user)).get(bucketId, flowId).getVersionCount();
     }
 
     @Override
-    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version) throws IOException, NiFiRegistryException {
-        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient();
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
         final VersionedFlowSnapshot flowSnapshot = snapshotClient.get(bucketId, flowId, version);
 
         final VersionedProcessGroup contents = flowSnapshot.getFlowContents();
         for (final VersionedProcessGroup child : contents.getProcessGroups()) {
-            populateVersionedContentsRecursively(child);
+            populateVersionedContentsRecursively(child, user);
         }
 
         return flowSnapshot;
     }
 
-    private void populateVersionedContentsRecursively(final VersionedProcessGroup group) throws NiFiRegistryException, IOException {
+    @Override
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version) throws IOException, NiFiRegistryException {
+        return getFlowContents(bucketId, flowId, version, null);
+    }
+
+    private void populateVersionedContentsRecursively(final VersionedProcessGroup group, final NiFiUser user) throws NiFiRegistryException, IOException {
         if (group == null) {
             return;
         }
@@ -205,7 +214,7 @@ public class RestBasedFlowRegistry implements FlowRegistry {
             }
 
             final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
-            final VersionedFlowSnapshot snapshot = flowRegistry.getFlowContents(bucketId, flowId, version);
+            final VersionedFlowSnapshot snapshot = flowRegistry.getFlowContents(bucketId, flowId, version, user);
             final VersionedProcessGroup contents = snapshot.getFlowContents();
 
             group.setComments(contents.getComments());
@@ -222,14 +231,19 @@ public class RestBasedFlowRegistry implements FlowRegistry {
         }
 
         for (final VersionedProcessGroup child : group.getProcessGroups()) {
-            populateVersionedContentsRecursively(child);
+            populateVersionedContentsRecursively(child, user);
         }
     }
 
     @Override
+    public VersionedFlow getVersionedFlow(final String bucketId, final String flowId, final NiFiUser user) throws IOException, NiFiRegistryException {
+        final FlowClient flowClient = getRegistryClient().getFlowClient(getIdentity(user));
+        return flowClient.get(bucketId, flowId);
+    }
+
+    @Override
     public VersionedFlow getVersionedFlow(final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
         final FlowClient flowClient = getRegistryClient().getFlowClient();
         return flowClient.get(bucketId, flowId);
     }
-
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
index d5d0d86..8a2447d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
@@ -17,7 +17,6 @@
 
 package org.apache.nifi.registry.flow;
 
-import java.io.IOException;
 import java.net.URI;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -56,14 +55,15 @@ public class StandardFlowRegistryClient implements FlowRegistryClient {
         final String uriScheme = uri.getScheme();
 
         final FlowRegistry registry;
-        if (uriScheme.equalsIgnoreCase("file")) {
-            try {
-                registry = new FileBasedFlowRegistry(registryId, registryUrl);
-            } catch (IOException e) {
-                throw new RuntimeException("Failed to create Flow Registry for URI " + registryUrl, e);
+        if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
+            final SSLContext sslContext = SslContextFactory.createSslContext(nifiProperties, false);
+            if (sslContext == null && uriScheme.equalsIgnoreCase("https")) {
+                throw new RuntimeException("Failed to create Flow Registry for URI " + registryUrl
+                    + " because this NiFi is not configured with a Keystore/Truststore, so it is not capable of communicating with a secure Registry. "
+                    + "Please populate NiFi's Keystore/Truststore properties or connect to a NiFi Registry over http instead of https.");
             }
 
-            registry.setName(registryName);
+            registry = new RestBasedFlowRegistry(this, registryId, registryUrl, sslContext, registryName);
             registry.setDescription(description);
         } else if (uriScheme.equalsIgnoreCase("http") || uriScheme.equalsIgnoreCase("https")) {
             final SSLContext sslContext = SslContextFactory.createSslContext(nifiProperties, false);

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
index aaba126..92a4166 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
@@ -18,7 +18,6 @@
 package org.apache.nifi.registry.flow;
 
 import java.util.Objects;
-import java.util.Optional;
 
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 
@@ -33,8 +32,8 @@ public class StandardVersionControlInformation implements VersionControlInformat
     private volatile String flowDescription;
     private final int version;
     private volatile VersionedProcessGroup flowSnapshot;
-    private volatile Boolean modified = null;
-    private volatile Boolean current = null;
+    private volatile boolean modified;
+    private volatile boolean current;
 
     public static class Builder {
         private String registryIdentifier;
@@ -89,12 +88,12 @@ public class StandardVersionControlInformation implements VersionControlInformat
             return this;
         }
 
-        public Builder modified(Boolean modified) {
+        public Builder modified(boolean modified) {
             this.modified = modified;
             return this;
         }
 
-        public Builder current(Boolean current) {
+        public Builder current(boolean current) {
             this.current = current;
             return this;
         }
@@ -113,8 +112,8 @@ public class StandardVersionControlInformation implements VersionControlInformat
                 .flowId(dto.getFlowId())
                 .flowName(dto.getFlowName())
                 .flowDescription(dto.getFlowDescription())
-                .current(dto.getCurrent())
-                .modified(dto.getModified())
+                .current(dto.getCurrent() == null ? true : dto.getCurrent())
+                .modified(dto.getModified() == null ? false : dto.getModified())
                 .version(dto.getVersion());
 
             return builder;
@@ -139,7 +138,7 @@ public class StandardVersionControlInformation implements VersionControlInformat
 
 
     public StandardVersionControlInformation(final String registryId, final String registryName, final String bucketId, final String flowId, final int version,
-        final VersionedProcessGroup snapshot, final Boolean modified, final Boolean current) {
+        final VersionedProcessGroup snapshot, final boolean modified, final boolean current) {
         this.registryIdentifier = registryId;
         this.registryName = registryName;
         this.bucketIdentifier = bucketId;
@@ -208,13 +207,13 @@ public class StandardVersionControlInformation implements VersionControlInformat
     }
 
     @Override
-    public Optional<Boolean> getModified() {
-        return Optional.ofNullable(modified);
+    public boolean isModified() {
+        return modified;
     }
 
     @Override
-    public Optional<Boolean> getCurrent() {
-        return Optional.ofNullable(current);
+    public boolean isCurrent() {
+        return current;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index be907ba..76cd2c4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -1318,6 +1318,16 @@ public interface NiFiServiceFacade {
     VersionControlComponentMappingEntity registerFlowWithFlowRegistry(String groupId, StartVersionControlRequestEntity requestEntity);
 
     /**
+     * Deletes the specified Versioned Flow from the specified Flow Registry
+     *
+     * @param registryId the ID of the Flow Registry
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     * @return the VersionedFlow that was deleted
+     */
+    VersionedFlow deleteVersionedFlow(String registryId, String bucketId, String flowId) throws IOException, NiFiRegistryException;
+
+    /**
      * Adds the given snapshot to the already existing Versioned Flow, which resides in the given Flow Registry with the given id
      *
      * @param registryId the ID of the Flow Registry to persist the snapshot to

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 89e00ba..4d1bbbc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,7 +16,9 @@
  */
 package org.apache.nifi.web;
 
-import com.google.common.collect.Sets;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
@@ -88,9 +90,9 @@ import org.apache.nifi.history.HistoryQuery;
 import org.apache.nifi.history.PreviousValue;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedConnection;
@@ -228,6 +230,7 @@ import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
 import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
 import org.apache.nifi.web.api.entity.SnippetEntity;
+import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.StatusHistoryEntity;
 import org.apache.nifi.web.api.entity.TemplateEntity;
 import org.apache.nifi.web.api.entity.TenantEntity;
@@ -237,7 +240,6 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
-import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
@@ -268,8 +270,8 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
+import com.google.common.collect.Sets;
+
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -3667,8 +3669,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             action = "add the local flow to the Flow Registry as the first Snapshot";
 
             // add first snapshot to the flow in the registry
-            final String comments = versionedFlow.getDescription() == null ? "Initial version of flow" : versionedFlow.getDescription();
-            registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, comments, expectedVersion);
+            registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, versionedFlowDto.getComments(), expectedVersion);
         } catch (final NiFiRegistryException e) {
             throw new IllegalArgumentException(e);
         } catch (final IOException ioe) {
@@ -3676,14 +3677,17 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             throw new RuntimeException("Failed to communicate with Flow Registry when attempting to " + action);
         }
 
+        final Bucket bucket = registeredSnapshot.getBucket();
+        final VersionedFlow flow = registeredSnapshot.getFlow();
+
         // Update the Process Group with the new VersionControlInformation. (Send this to all nodes).
         final VersionControlInformationDTO vci = new VersionControlInformationDTO();
-        vci.setBucketId(registeredFlow.getBucketIdentifier());
-        vci.setBucketName(registeredFlow.getBucketName());
+        vci.setBucketId(bucket.getIdentifier());
+        vci.setBucketName(bucket.getName());
         vci.setCurrent(true);
-        vci.setFlowId(registeredFlow.getIdentifier());
-        vci.setFlowName(registeredFlow.getName());
-        vci.setFlowDescription(registeredFlow.getDescription());
+        vci.setFlowId(flow.getIdentifier());
+        vci.setFlowName(flow.getName());
+        vci.setFlowDescription(flow.getDescription());
         vci.setGroupId(groupId);
         vci.setModified(false);
         vci.setRegistryId(registryId);
@@ -3703,6 +3707,16 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
+    public VersionedFlow deleteVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
+        final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
+        if (registry == null) {
+            throw new IllegalArgumentException("No Flow Registry exists with ID " + registryId);
+        }
+
+        return registry.deleteVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
     public VersionControlInformationEntity getVersionControlInformation(final String groupId) {
         final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
         final VersionControlInformation versionControlInfo = processGroup.getVersionControlInformation();
@@ -3737,8 +3751,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         }
 
         final VersionedFlowSnapshot versionedFlowSnapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketIdentifier(),
-            versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion());
-
+            versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), NiFiUserUtils.getNiFiUser());
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
         final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, true);
@@ -3784,7 +3797,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.registerVersionedFlow(flow);
+        return registry.registerVersionedFlow(flow, NiFiUserUtils.getNiFiUser());
     }
 
     private VersionedFlow getVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, NiFiRegistryException {
@@ -3793,7 +3806,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.getVersionedFlow(bucketId, flowId);
+        return registry.getVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser());
     }
 
     @Override
@@ -3804,7 +3817,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
         }
 
-        return registry.registerVersionedFlowSnapshot(flow, snapshot, comments, expectedVersion);
+        return registry.registerVersionedFlowSnapshot(flow, snapshot, comments, expectedVersion, NiFiUserUtils.getNiFiUser());
     }
 
     @Override
@@ -4023,7 +4036,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
         final VersionedFlowSnapshot snapshot;
         try {
-            snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion());
+            snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion(), NiFiUserUtils.getNiFiUser());
         } catch (final NiFiRegistryException e) {
             throw new IllegalArgumentException("The Flow Registry with ID " + versionControlInfo.getRegistryId() + " reports that no Flow exists with Bucket "
                 + versionControlInfo.getBucketId() + ", Flow " + versionControlInfo.getFlowId() + ", Version " + versionControlInfo.getVersion());
@@ -4064,7 +4077,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
             final VersionedFlowSnapshot childSnapshot;
             try {
-                childSnapshot = flowRegistry.getFlowContents(remoteCoordinates.getBucketId(), remoteCoordinates.getFlowId(), remoteCoordinates.getVersion());
+                childSnapshot = flowRegistry.getFlowContents(remoteCoordinates.getBucketId(), remoteCoordinates.getFlowId(), remoteCoordinates.getVersion(), NiFiUserUtils.getNiFiUser());
             } catch (final NiFiRegistryException e) {
                 throw new IllegalArgumentException("The Flow Registry with ID " + registryId + " reports that no Flow exists with Bucket "
                     + remoteCoordinates.getBucketId() + ", Flow " + remoteCoordinates.getFlowId() + ", Version " + remoteCoordinates.getVersion());

http://git-wip-us.apache.org/repos/asf/nifi/blob/f6cc5b6c/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index d24dcbb..fe57dd6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -87,10 +87,11 @@ import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.serialization.FlowEncodingVersion;
 import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.FlowRegistryUtils;
+import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
-import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateStep;
 import org.apache.nifi.remote.util.SiteToSiteRestApiClient;
@@ -1643,11 +1644,12 @@ public class ProcessGroupResource extends ApplicationResource {
             // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
             // Step 2: Retrieve flow from Flow Registry
             final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(versionControlInfo);
+            final Bucket bucket = flowSnapshot.getBucket();
+            final VersionedFlow flow = flowSnapshot.getFlow();
 
-            final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
-            versionControlInfo.setBucketName(metadata.getBucketName());
-            versionControlInfo.setFlowName(metadata.getFlowName());
-            versionControlInfo.setFlowDescription(metadata.getFlowDescription());
+            versionControlInfo.setBucketName(bucket.getName());
+            versionControlInfo.setFlowName(flow.getName());
+            versionControlInfo.setFlowDescription(flow.getDescription());
 
             versionControlInfo.setRegistryName(serviceFacade.getFlowRegistryName(versionControlInfo.getRegistryId()));
 


[26/50] nifi git commit: NIFI-4436: - Updating front end to use version control state/status. - Fixing copy/paste issue during revert local changes. - Code clean up in the breadcrumbs. - Update VersionsResource authorization and two phase commit object u

Posted by bb...@apache.org.
NIFI-4436:
- Updating front end to use version control state/status.
- Fixing copy/paste issue during revert local changes.
- Code clean up in the breadcrumbs.
- Update VersionsResource authorization and two phase commit object usage.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/49aad2c3
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/49aad2c3
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/49aad2c3

Branch: refs/heads/master
Commit: 49aad2c3a8a56602712ff3ede78a29221b492f3d
Parents: fdef5b5
Author: Matt Gilman <ma...@gmail.com>
Authored: Mon Dec 4 16:11:11 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:54 2018 -0500

----------------------------------------------------------------------
 .../api/entity/CreateActiveRequestEntity.java   |  36 ++++
 .../apache/nifi/web/api/VersionsResource.java   | 210 ++++++++++++-------
 .../WEB-INF/partials/canvas/navigation.jsp      |   3 +-
 .../nifi-web-ui/src/main/webapp/css/main.css    |  12 +-
 .../controllers/nf-ng-breadcrumbs-controller.js |  37 ++--
 .../directives/nf-ng-breadcrumbs-directive.js   |   3 +-
 .../main/webapp/js/nf/canvas/nf-context-menu.js |   6 +-
 .../main/webapp/js/nf/canvas/nf-flow-version.js |  23 +-
 .../webapp/js/nf/canvas/nf-process-group.js     | 109 +++++++---
 .../src/main/webapp/js/nf/nf-common.js          |  12 +-
 .../views/nf-ng-breadcrumbs-directive-view.html |   9 +-
 11 files changed, 293 insertions(+), 167 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
new file mode 100644
index 0000000..63a07a2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.web.api.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "createActiveRequest")
+public class CreateActiveRequestEntity extends Entity {
+    private String processGroupId;
+
+    @ApiModelProperty("The Process Group ID that this active request will update")
+    public String getProcessGroupId() {
+        return processGroupId;
+    }
+
+    public void setProcessGroupId(String processGroupId) {
+        this.processGroupId = processGroupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 6e61182..245713e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,23 +17,14 @@
 
 package org.apache.nifi.web.api;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.authorization.AccessDeniedException;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.ProcessGroupAuthorizable;
 import org.apache.nifi.authorization.RequestAction;
@@ -65,6 +56,7 @@ import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 import org.apache.nifi.web.api.dto.VersionedFlowUpdateRequestDTO;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.CreateActiveRequestEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
@@ -80,6 +72,21 @@ import org.apache.nifi.web.util.LifecycleManagementException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -95,13 +102,6 @@ import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
-
 @Path("/versions")
 @Api(value = "/versions", description = "Endpoint for managing version control for a flow")
 public class VersionsResource extends ApplicationResource {
@@ -168,7 +168,7 @@ public class VersionsResource extends ApplicationResource {
     @POST
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("start-requests")
+    @Path("active-requests")
     @ApiOperation(
         value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed. Creating this request will "
             + "prevent any other threads from simultaneously saving local changes to Version Control. It will not, however, actually save the local flow to the Flow Registry. A "
@@ -182,20 +182,28 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 404, message = "The specified resource could not be found."),
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
-    public Response createVersionControlRequest() throws InterruptedException {
+    public Response createVersionControlRequest(
+            @ApiParam(value = "The versioned flow details.", required = true) final CreateActiveRequestEntity requestEntity) throws InterruptedException {
 
         if (isReplicateRequest()) {
             return replicate(HttpMethod.POST);
         }
 
+        if (requestEntity.getProcessGroupId() == null) {
+            throw new IllegalArgumentException("The id of the process group that will be updated must be specified.");
+        }
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
         return withWriteLock(
             serviceFacade,
-            /* entity */ null,
+            requestEntity,
             lookup -> {
-                // TODO - pass in PG ID to authorize
+                final Authorizable processGroup = lookup.getProcessGroup(requestEntity.getProcessGroupId()).getAuthorizable();
+                processGroup.authorize(authorizer, RequestAction.WRITE, user);
             },
             /* verifier */ null,
-            requestEntity -> {
+            entity -> {
                 final String requestId = generateUuid();
 
                 // We need to ensure that only a single Version Control Request can occur throughout the flow.
@@ -204,7 +212,7 @@ public class VersionsResource extends ApplicationResource {
                 // As a result, may could end up in a situation where we are creating flows in the registry that are never referenced.
                 synchronized (activeRequestMonitor) {
                     if (activeRequest == null || activeRequest.isExpired()) {
-                        activeRequest = new ActiveRequest(requestId);
+                        activeRequest = new ActiveRequest(requestId, user, entity.getProcessGroupId());
                     } else {
                         throw new IllegalStateException("A request is already underway to place a Process Group in this NiFi instance under Version Control. "
                             + "Only a single such request is allowed to occurred at a time. Please try the request again momentarily.");
@@ -219,7 +227,7 @@ public class VersionsResource extends ApplicationResource {
     @PUT
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("start-requests/{id}")
+    @Path("active-requests/{id}")
     @ApiOperation(
             value = "Updates the request with the given ID",
             response = VersionControlInformationEntity.class,
@@ -235,7 +243,7 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
     public Response updateVersionControlRequest(@ApiParam("The request ID.") @PathParam("id") final String requestId,
-        @ApiParam(value = "The controller service configuration details.", required = true) final VersionControlComponentMappingEntity requestEntity) {
+        @ApiParam(value = "The version control component mapping.", required = true) final VersionControlComponentMappingEntity requestEntity) {
 
         // Verify request
         final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
@@ -287,21 +295,40 @@ public class VersionsResource extends ApplicationResource {
                 throw new IllegalStateException("Version Control Request with ID " + requestId + " has already expired");
             }
 
+            if (activeRequest.isUpdatePerformed()) {
+                throw new IllegalStateException("Version Control Request with ID " + requestId + " has already been performed");
+            }
+
             final String groupId = requestEntity.getVersionControlInformation().getGroupId();
 
+            if (!activeRequest.getProcessGroupId().equals(groupId)) {
+                throw new IllegalStateException("Version Control Request with ID " + requestId + " was created for a different process group id");
+            }
+
             final Revision groupRevision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
             return withWriteLock(
                 serviceFacade,
                 requestEntity,
                 groupRevision,
                 lookup -> {
-                    final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
-                    processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                    final NiFiUser user = NiFiUserUtils.getNiFiUser();
+                    if (user == null) {
+                        throw new AccessDeniedException("Unknown user.");
+                    }
+
+                    if (!user.equals(activeRequest.getUser())) {
+                        throw new AccessDeniedException("Only the user that creates the Version Control Request can use it.");
+                    }
                 },
                 null,
                 (rev, mappingEntity) -> {
+                    // set the version control information
                     final VersionControlInformationEntity responseEntity = serviceFacade.setVersionControlInformation(rev, groupId,
                         mappingEntity.getVersionControlInformation(), mappingEntity.getVersionControlComponentMapping());
+
+                    // indicate that the active request has performed the update
+                    activeRequest.updatePerformed();
+
                     return generateOkResponse(responseEntity).build();
                 });
         }
@@ -311,10 +338,10 @@ public class VersionsResource extends ApplicationResource {
     @DELETE
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
-    @Path("start-requests/{id}")
+    @Path("active-requests/{id}")
     @ApiOperation(
         value = "Deletes the Version Control Request with the given ID. This will allow other threads to save flows to the Flow Registry. See also the documentation "
-            + "for POSTing to /versions/start-requests for information regarding why this is done.",
+            + "for POSTing to /versions/active-requests for information regarding why this is done.",
             notes = NON_GUARANTEED_ENDPOINT)
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -328,28 +355,36 @@ public class VersionsResource extends ApplicationResource {
             return replicate(HttpMethod.DELETE);
         }
 
-        return withWriteLock(
-            serviceFacade,
-            null,
-            lookup -> {
-            },
-            null,
-            requestEntity -> {
-                synchronized (activeRequestMonitor) {
-                    if (activeRequest == null) {
-                        throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
-                    }
+        synchronized (activeRequestMonitor) {
+            if (activeRequest == null) {
+                throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+            }
 
-                    if (!requestId.equals(activeRequest.getRequestId())) {
-                        throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+            if (!requestId.equals(activeRequest.getRequestId())) {
+                throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+            }
+
+            return withWriteLock(
+                serviceFacade,
+                null,
+                lookup -> {
+                    final NiFiUser user = NiFiUserUtils.getNiFiUser();
+                    if (user == null) {
+                        throw new AccessDeniedException("Unknown user.");
                     }
 
+                    if (!user.equals(activeRequest.getUser())) {
+                        throw new AccessDeniedException("Only the user that creates the Version Control Request can use it.");
+                    }
+                },
+                null,
+                requestEntity -> {
+                    // clear the active request
                     activeRequest = null;
-                }
-
-                return generateOkResponse().build();
-            });
 
+                    return generateOkResponse().build();
+                });
+        }
     }
 
 
@@ -407,7 +442,7 @@ public class VersionsResource extends ApplicationResource {
 
         if (isReplicateRequest()) {
             // We first have to obtain a "lock" on all nodes in the cluster so that multiple Version Control requests
-            // are not being made simultaneously. We do this by making a POST to /nifi-api/versions/start-requests.
+            // are not being made simultaneously. We do this by making a POST to /nifi-api/versions/active-requests.
             // The Response gives us back the Request ID.
             final URI requestUri;
             try {
@@ -415,7 +450,7 @@ public class VersionsResource extends ApplicationResource {
                 final String requestId = lockVersionControl(originalUri, groupId);
 
                 requestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
-                    originalUri.getPort(), "/nifi-api/versions/start-requests/" + requestId, null, originalUri.getFragment());
+                    originalUri.getPort(), "/nifi-api/versions/active-requests/" + requestId, null, originalUri.getFragment());
             } catch (final URISyntaxException e) {
                 throw new RuntimeException(e);
             }
@@ -448,9 +483,12 @@ public class VersionsResource extends ApplicationResource {
             lookup -> {
                 final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
                 final Authorizable processGroup = groupAuthorizable.getAuthorizable();
-                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+
+                // require write to this group
                 processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
+
+                // require read to this group and all descendants
+                authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
             },
             () -> {
                 final VersionedFlowDTO versionedFlow = requestEntity.getVersionedFlow();
@@ -493,15 +531,19 @@ public class VersionsResource extends ApplicationResource {
 
     private String lockVersionControl(final URI originalUri, final String groupId) throws URISyntaxException {
         final URI createRequestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
-            originalUri.getPort(), "/nifi-api/versions/start-requests", null, originalUri.getFragment());
+            originalUri.getPort(), "/nifi-api/versions/active-requests", null, originalUri.getFragment());
 
         final NodeResponse clusterResponse;
         try {
+            // create an active request entity to indicate the group id
+            final CreateActiveRequestEntity activeRequestEntity = new CreateActiveRequestEntity();
+            activeRequestEntity.setProcessGroupId(groupId);
+
             if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, activeRequestEntity, Collections.emptyMap()).awaitMergedResponse();
             } else {
                 clusterResponse = getRequestReplicator().forwardToCoordinator(
-                    getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
+                    getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, activeRequestEntity, Collections.emptyMap()).awaitMergedResponse();
             }
         } catch (final InterruptedException ie) {
             Thread.currentThread().interrupt();
@@ -625,7 +667,7 @@ public class VersionsResource extends ApplicationResource {
             },
             (revision, groupEntity) -> {
                 // disconnect from version control
-                final VersionControlInformationEntity entity = serviceFacade.deleteVersionControl(requestRevision, groupId);
+                final VersionControlInformationEntity entity = serviceFacade.deleteVersionControl(revision, groupId);
 
                 // generate the response
                 return generateOkResponse(entity).build();
@@ -716,8 +758,8 @@ public class VersionsResource extends ApplicationResource {
                 versionControlInfoDto.setGroupId(groupId);
                 versionControlInfoDto.setModified(false);
                 versionControlInfoDto.setVersion(snapshotMetadata.getVersion());
-                versionControlInfoDto.setRegistryId(requestEntity.getRegistryId());
-                versionControlInfoDto.setRegistryName(serviceFacade.getFlowRegistryName(requestEntity.getRegistryId()));
+                versionControlInfoDto.setRegistryId(entity.getRegistryId());
+                versionControlInfoDto.setRegistryName(serviceFacade.getFlowRegistryName(entity.getRegistryId()));
 
                 final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroup(rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false,
                     entity.getUpdateDescendantVersionedFlows());
@@ -785,6 +827,7 @@ public class VersionsResource extends ApplicationResource {
 
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
+        // request manager will ensure that the current is the user that submitted this request
         final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest = requestManager.getRequest(requestType, requestId, user);
 
         final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
@@ -865,6 +908,7 @@ public class VersionsResource extends ApplicationResource {
 
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
+        // request manager will ensure that the current is the user that submitted this request
         final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest = requestManager.removeRequest(requestType, requestId, user);
         if (asyncRequest == null) {
             throw new ResourceNotFoundException("Could not find request of type " + requestType + " with ID " + requestId);
@@ -1021,11 +1065,8 @@ public class VersionsResource extends ApplicationResource {
             lookup -> {
                 // Step 2: Verify READ and WRITE permissions for user, for every component.
                 final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
-                final Authorizable processGroup = groupAuthorizable.getAuthorizable();
-                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
-                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.WRITE, true, false, true, true);
+                authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
+                authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.WRITE, true, false, true, true);
 
                 final VersionedProcessGroup groupContents = flowSnapshot.getFlowContents();
                 final boolean containsRestrictedComponents = FlowRegistryUtils.containsRestrictedComponent(groupContents);
@@ -1049,7 +1090,7 @@ public class VersionsResource extends ApplicationResource {
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
                         final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, true, true);
+                            affectedComponents, user, replicateRequest, processGroupEntity, flowSnapshot, request, idGenerationSeed, true, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final LifecycleManagementException e) {
@@ -1170,11 +1211,8 @@ public class VersionsResource extends ApplicationResource {
             lookup -> {
                 // Step 2: Verify READ and WRITE permissions for user, for every component.
                 final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
-                final Authorizable processGroup = groupAuthorizable.getAuthorizable();
-                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
-                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.WRITE, true, false, true, true);
+                authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
+                authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.WRITE, true, false, true, true);
 
                 final VersionedProcessGroup groupContents = flowSnapshot.getFlowContents();
                 final boolean containsRestrictedComponents = FlowRegistryUtils.containsRestrictedComponent(groupContents);
@@ -1217,7 +1255,7 @@ public class VersionsResource extends ApplicationResource {
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
                         final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false, true);
+                            affectedComponents, user, replicateRequest, processGroupEntity, flowSnapshot, request, idGenerationSeed, false, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final LifecycleManagementException e) {
@@ -1471,11 +1509,17 @@ public class VersionsResource extends ApplicationResource {
         private static final long MAX_REQUEST_LOCK_NANOS = TimeUnit.MINUTES.toNanos(1L);
 
         private final String requestId;
+        private final NiFiUser user;
+        private final String processGroupId;
         private final long creationNanos = System.nanoTime();
         private final long expirationTime = creationNanos + MAX_REQUEST_LOCK_NANOS;
 
-        private ActiveRequest(final String requestId) {
+        private boolean updatePerformed = false;
+
+        private ActiveRequest(final String requestId, final NiFiUser user, final String processGroupId) {
             this.requestId = requestId;
+            this.user = user;
+            this.processGroupId = processGroupId;
         }
 
         public boolean isExpired() {
@@ -1485,5 +1529,21 @@ public class VersionsResource extends ApplicationResource {
         public String getRequestId() {
             return requestId;
         }
+
+        public NiFiUser getUser() {
+            return user;
+        }
+
+        public String getProcessGroupId() {
+            return processGroupId;
+        }
+
+        public void updatePerformed() {
+            updatePerformed = true;
+        }
+
+        public boolean isUpdatePerformed() {
+            return updatePerformed;
+        }
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
index 4700b8b..f122076 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
@@ -21,8 +21,7 @@
         highlight-crumb-id="appCtrl.nf.CanvasUtils.getGroupId();"
         separator-func="appCtrl.nf.Common.isDefinedAndNotNull"
         is-tracking="appCtrl.serviceProvider.breadcrumbsCtrl.isTracking"
-        is-current="appCtrl.serviceProvider.breadcrumbsCtrl.isCurrent"
-        is-modified="appCtrl.serviceProvider.breadcrumbsCtrl.isModified"
+        get-version-control-class="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlClass"
         get-version-control-tooltip="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlTooltip">
 </nf-breadcrumbs>
 <div id="graph-controls">

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
index d10761a..2f8dd7b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
@@ -161,7 +161,7 @@ div.context-menu-provenance {
     text-shadow: 0 0 4px rgba(255,255,255,1);
 }
 
-.locally-modified {
+.locally-modified, .sync-failure {
     float: left;
     color: #747474 !important;
     fill: #747474 !important;
@@ -169,15 +169,7 @@ div.context-menu-provenance {
     text-shadow: 0 0 4px rgba(255,255,255,1);
 }
 
-.stale {
-    float: left;
-    color: #c7685d !important;
-    fill: #c7685d !important;
-    margin-top: 0px !important;
-    text-shadow: 0 0 4px rgba(255,255,255,1);
-}
-
-.locally-modified-and-stale {
+.stale, .locally-modified-and-stale {
     float: left;
     color: #c7685d !important;
     fill: #c7685d !important;

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
index 0b71e3c..fc5599d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
@@ -107,31 +107,28 @@
             },
 
             /**
-             * Returns whether the specified version control information is current.
+             * Returns the class string to use for the version control of the specified breadcrumb.
              *
              * @param breadcrumbEntity
-             * @returns {boolean}
+             * @returns {string}
              */
-            isCurrent: function (breadcrumbEntity) {
+            getVersionControlClass: function (breadcrumbEntity) {
                 if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
-                    return breadcrumbEntity.breadcrumb.versionControlInformation.current === true;
-                }
-
-                return false;
-            },
-
-            /**
-             * Returns whether the specified version control information is current.
-             *
-             * @param versionControlInformation
-             * @returns {boolean}
-             */
-            isModified: function (breadcrumbEntity) {
-                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation)) {
-                    return breadcrumbEntity.breadcrumb.versionControlInformation.modified === true;
+                    var vciState = breadcrumbEntity.breadcrumb.versionControlInformation.state;
+                    if (vciState === 'SYNC_FAILURE') {
+                        return 'breadcrumb-version-control-gray fa fa-question'
+                    } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
+                        return 'breadcrumb-version-control-red fa fa-exclamation-circle';
+                    } else if (vciState === 'STALE') {
+                        return 'breadcrumb-version-control-red fa fa-arrow-circle-up';
+                    } else if (vciState === 'LOCALLY_MODIFIED') {
+                        return 'breadcrumb-version-control-gray fa fa-asterisk';
+                    } else {
+                        return 'breadcrumb-version-control-green fa fa-check';
+                    }
+                } else {
+                    return '';
                 }
-
-                return false;
             },
 
             /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
index 1a549d7..b7a8f26 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/directives/nf-ng-breadcrumbs-directive.js
@@ -42,8 +42,7 @@
                 'highlightCrumbId': '=',
                 'separatorFunc': '=',
                 'isTracking': '=',
-                'isCurrent': '=',
-                'isModified': '=',
+                'getVersionControlClass': '=',
                 'getVersionControlTooltip': '='
             },
             link: function (scope, element, attrs) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index 616e151..0c21838 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -463,7 +463,7 @@
         }
 
         // check the selection for version control information
-        return versionControlInformation.current === true && versionControlInformation.modified === true;
+        return versionControlInformation.state === 'LOCALLY_MODIFIED';
     };
 
     /**
@@ -502,7 +502,7 @@
         }
 
         // check the selection for version control information
-        return versionControlInformation.modified === true;
+        return versionControlInformation.state === 'LOCALLY_MODIFIED' || versionControlInformation.state === 'LOCALLY_MODIFIED_AND_STALE';
     };
 
     /**
@@ -541,7 +541,7 @@
         }
 
         // check the selection for version control information
-        return versionControlInformation.modified === false;
+        return versionControlInformation.state !== 'LOCALLY_MODIFIED' && versionControlInformation.state !== 'LOCALLY_MODIFIED_AND_STALE';
     };
 
     /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 03aed70..9ef210e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -227,10 +227,11 @@
      *
      * @param registryIdentifier
      * @param bucketCombo
+     * @param flowCombo
      * @param selectBucket
      * @returns {*}
      */
-    var loadBuckets = function (registryIdentifier, bucketCombo, selectBucket) {
+    var loadBuckets = function (registryIdentifier, bucketCombo, flowCombo, selectBucket) {
         return $.ajax({
             type: 'GET',
             url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets',
@@ -258,6 +259,14 @@
                     optionClass: 'unset',
                     disabled: true
                 });
+                flowCombo.combo('destroy').combo({
+                    options: [{
+                        text: 'No available flows',
+                        value: null,
+                        optionClass: 'unset',
+                        disabled: true
+                    }]
+                });
             }
 
             // load the buckets
@@ -287,6 +296,14 @@
                     disabled: true
                 }]
             });
+            flowCombo.combo('destroy').combo({
+                options: [{
+                    text: 'No available flows',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
 
             dialog.modal('refreshButtons');
         };
@@ -316,7 +333,7 @@
                 clearFlowVersionsGrid();
             }
 
-            loadBuckets(selectedOption.value, bucketCombo, selectBucket).fail(function () {
+            loadBuckets(selectedOption.value, bucketCombo, flowCombo, selectBucket).fail(function () {
                 showNoBucketsAvailable();
             });
         }
@@ -1439,7 +1456,7 @@
 
                                 nfDialog.showOkDialog({
                                     headerText: 'Revert Local Changes',
-                                    dialogContent: nfCommon.escapeHtml(changeRequest.failureReason)
+                                    dialogContent: nfCommon.escapeHtml(revertRequest.failureReason)
                                 });
                             } else {
                                 // update the percent complete

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 69db138..111636b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -397,7 +397,7 @@
                             'class': 'process-group-disabled-count process-group-contents-count'
                         });
 
-                    // current icon
+                    // up to date icon
                     details.append('text')
                         .attr({
                             'x': 10,
@@ -409,7 +409,7 @@
                         })
                         .text('\uf00c');
 
-                    // current count
+                    // up to date count
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -418,7 +418,7 @@
                             'class': 'process-group-up-to-date-count process-group-contents-count'
                         });
 
-                    // modified icon
+                    // locally modified icon
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -429,7 +429,7 @@
                         })
                         .text('\uf069');
 
-                    // modified count
+                    // locally modified count
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -438,7 +438,7 @@
                             'class': 'process-group-locally-modified-count process-group-contents-count'
                         });
 
-                    // not current icon
+                    // stale icon
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -449,7 +449,7 @@
                         })
                         .text('\uf0aa');
 
-                    // not current count
+                    // stale count
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -458,7 +458,7 @@
                             'class': 'process-group-stale-count process-group-contents-count'
                         });
 
-                    // modified and not current icon
+                    // locally modified and stale icon
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -469,7 +469,7 @@
                         })
                         .text('\uf06a');
 
-                    // modified and not current count
+                    // locally modified and stale count
                     details.append('text')
                         .attr({
                             'y': function () {
@@ -478,6 +478,26 @@
                             'class': 'process-group-locally-modified-and-stale-count process-group-contents-count'
                         });
 
+                    // sync failure icon
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-sync-failure process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf128');
+
+                    // sync failure count
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-sync-failure-count process-group-contents-count'
+                        });
+
                     // ----------------
                     // stats background
                     // ----------------
@@ -940,13 +960,14 @@
                             'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
                             'fill': function () {
                                 if (isUnderVersionControl(processGroupData)) {
-                                    var modified = processGroupData.component.versionControlInformation.modified;
-                                    var current = processGroupData.component.versionControlInformation.current;
-                                    if (modified === true && current === false) {
+                                    var vciState = processGroupData.component.versionControlInformation.state;
+                                    if (vciState === 'SYNC_FAILURE') {
+                                        return '#666666';
+                                    } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
                                         return '#BA554A';
-                                    } else if (current === false) {
+                                    } else if (vciState === 'STALE') {
                                         return '#BA554A';
-                                    } else if (modified === true) {
+                                    } else if (vciState === 'LOCALLY_MODIFIED') {
                                         return '#666666';
                                     } else {
                                         return '#1A9964';
@@ -958,13 +979,14 @@
                         })
                         .text(function () {
                             if (isUnderVersionControl(processGroupData)) {
-                                var modified = processGroupData.component.versionControlInformation.modified;
-                                var current = processGroupData.component.versionControlInformation.current;
-                                if (modified === true && current === false) {
+                                var vciState = processGroupData.component.versionControlInformation.state;
+                                if (vciState === 'SYNC_FAILURE') {
+                                    return '\uf128'
+                                } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
                                     return '\uf06a';
-                                } else if (current === false) {
+                                } else if (vciState === 'STALE') {
                                     return '\uf0aa';
-                                } else if (modified === true) {
+                                } else if (vciState === 'LOCALLY_MODIFIED') {
                                     return '\uf069';
                                 } else {
                                     return '\uf00c';
@@ -1081,8 +1103,8 @@
                         });
                     var upToDateCount = details.select('text.process-group-up-to-date-count')
                         .attr('x', function () {
-                            var currentCountX = parseInt(upToDate.attr('x'), 10);
-                            return currentCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                            var updateToDateCountX = parseInt(upToDate.attr('x'), 10);
+                            return updateToDateCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                         })
                         .text(function (d) {
                             return d.component.upToDateCount;
@@ -1097,13 +1119,13 @@
                             return d.component.locallyModifiedCount === 0;
                         })
                         .attr('x', function () {
-                            var currentX = parseInt(upToDateCount.attr('x'), 10);
-                            return currentX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                            var upToDateX = parseInt(upToDateCount.attr('x'), 10);
+                            return upToDateX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                         });
                     var locallyModifiedCount = details.select('text.process-group-locally-modified-count')
                         .attr('x', function () {
-                            var modifiedCountX = parseInt(locallyModified.attr('x'), 10);
-                            return modifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                            var locallyModifiedCountX = parseInt(locallyModified.attr('x'), 10);
+                            return locallyModifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                         })
                         .text(function (d) {
                             return d.component.locallyModifiedCount;
@@ -1118,13 +1140,13 @@
                             return d.component.staleCount === 0;
                         })
                         .attr('x', function () {
-                            var modifiedX = parseInt(locallyModifiedCount.attr('x'), 10);
-                            return modifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                            var locallyModifiedX = parseInt(locallyModifiedCount.attr('x'), 10);
+                            return locallyModifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                         });
                     var staleCount = details.select('text.process-group-stale-count')
                         .attr('x', function () {
-                            var notCurrentCountX = parseInt(stale.attr('x'), 10);
-                            return notCurrentCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                            var staleCountX = parseInt(stale.attr('x'), 10);
+                            return staleCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                         })
                         .text(function (d) {
                             return d.component.staleCount;
@@ -1139,17 +1161,38 @@
                             return d.component.locallyModifiedAndStaleCount === 0;
                         })
                         .attr('x', function () {
-                            var runningX = parseInt(staleCount.attr('x'), 10);
-                            return runningX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                            var staleX = parseInt(staleCount.attr('x'), 10);
+                            return staleX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
                         });
-                    details.select('text.process-group-locally-modified-and-stale-count')
+                    var locallyModifiedAndStaleCount = details.select('text.process-group-locally-modified-and-stale-count')
                         .attr('x', function () {
-                            var modifiedAndNotCurrentCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
-                            return modifiedAndNotCurrentCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                            var locallyModifiedAndStaleCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
+                            return locallyModifiedAndStaleCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
                         })
                         .text(function (d) {
                             return d.component.locallyModifiedAndStaleCount;
                         });
+
+                    // update sync failure
+                    var syncFailure = details.select('text.process-group-sync-failure')
+                        .classed('sync-failure', function (d) {
+                            return d.component.syncFailureCount > 0;
+                        })
+                        .classed('zero', function (d) {
+                            return d.component.syncFailureCount === 0;
+                        })
+                        .attr('x', function () {
+                            var syncFailureX = parseInt(locallyModifiedAndStaleCount.attr('x'), 10);
+                            return syncFailureX + Math.round(locallyModifiedAndStaleCount.node().getComputedTextLength()) + CONTENTS_SPACER - 2;
+                        });
+                    details.select('text.process-group-sync-failure-count')
+                        .attr('x', function () {
+                            var syncFailureCountX = parseInt(syncFailure.attr('x'), 10);
+                            return syncFailureCountX + Math.round(syncFailure.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                        })
+                        .text(function (d) {
+                            return d.component.syncFailureCount;
+                        });
                 } else {
                     // update version control information
                     processGroup.select('text.version-control').style('visibility', false).text('');

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index b3671a7..a1a3fc6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -425,17 +425,7 @@
          * @param versionControlInformation
          */
         getVersionControlTooltip: function (versionControlInformation) {
-            var modified = versionControlInformation.modified;
-            var current = versionControlInformation.current;
-            if (modified === true && current === false) {
-                return 'Local changes have been made and a newer version of this flow is available';
-            } else if (current === false) {
-                return 'A newer version of this flow is available';
-            } else if (modified === true) {
-                return 'Local changes have been made';
-            } else {
-                return 'Flow version is current';
-            }
+            return versionControlInformation.stateExplanation;
         },
 
         /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/49aad2c3/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
index 3e5d87b..a69de46 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
@@ -22,14 +22,7 @@ limitations under the License.
                 <span ng-if="separatorFunc(crumb.parentBreadcrumb)" style="margin: 0 12px;">
                     &raquo;
                 </span>
-                <span ng-if="isTracking(crumb) && isCurrent(crumb) && !isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
-                      class="breadcrumb-version-control-green fa fa-check" style="margin: 0 6px;"></span>
-                <span ng-if="isTracking(crumb) && isCurrent(crumb) && isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
-                      class="breadcrumb-version-control-gray fa fa-asterisk" style="margin: 0 6px;"></span>
-                <span ng-if="isTracking(crumb) && !isCurrent(crumb) && !isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
-                      class="breadcrumb-version-control-red fa fa-arrow-circle-up" style="margin: 0 6px;"></span>
-                <span ng-if="isTracking(crumb) && !isCurrent(crumb) && isModified(crumb)" title="{{getVersionControlTooltip(crumb)}}"
-                      class="breadcrumb-version-control-red fa fa-exclamation-circle" style="margin: 0 6px;"></span>
+                <span ng-if="isTracking(crumb)" title="{{getVersionControlTooltip(crumb)}}" class="{{getVersionControlClass(crumb)}}" style="margin: 0 6px;"></span>
                 <span class="link"
                       ng-class="(highlightCrumbId === crumb.id) ? 'link-bold' : ''"
                       ng-click="clickFunc(crumb.id)">


[12/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
index 1dc74ab..23f723e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
@@ -36,6 +36,7 @@ import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.SystemBundle;
 import org.apache.nifi.provenance.MockProvenanceRepository;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.variable.FileBasedVariableRegistry;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.util.NiFiProperties;
@@ -64,6 +65,7 @@ public class TestStandardReportingContext {
     private Bundle systemBundle;
     private BulletinRepository bulletinRepo;
     private VariableRegistry variableRegistry;
+    private FlowRegistryClient flowRegistry;
     private volatile String propsFile = TestStandardReportingContext.class.getResource("/flowcontrollertest.nifi.properties").getFile();
 
     @Before
@@ -120,9 +122,10 @@ public class TestStandardReportingContext {
 
         authorizer = new MockPolicyBasedAuthorizer(groups1, users1, policies1);
         variableRegistry = new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths());
+        flowRegistry = Mockito.mock(FlowRegistryClient.class);
 
         bulletinRepo = Mockito.mock(BulletinRepository.class);
-        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer, auditService, encryptor, bulletinRepo, variableRegistry);
+        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer, auditService, encryptor, bulletinRepo, variableRegistry, flowRegistry);
     }
 
     @After

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestProcessorLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestProcessorLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestProcessorLifecycle.java
index b55e98d..1b54c64 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestProcessorLifecycle.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestProcessorLifecycle.java
@@ -46,6 +46,7 @@ import org.apache.nifi.processor.ProcessSession;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.provenance.MockProvenanceRepository;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.variable.FileBasedVariableRegistry;
 import org.apache.nifi.util.NiFiProperties;
 import org.junit.After;
@@ -714,7 +715,8 @@ public class TestProcessorLifecycle {
 
         final FlowController flowController = FlowController.createStandaloneInstance(mock(FlowFileEventRepository.class), nifiProperties,
                 mock(Authorizer.class), mock(AuditService.class), null, new VolatileBulletinRepository(),
-                new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths()));
+            new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths()),
+            mock(FlowRegistryClient.class));
 
         return new FlowControllerAndSystemBundle(flowController, systemBundle);
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
index 7a49103..8044ede 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/StandardFlowSerializerTest.java
@@ -29,6 +29,7 @@ import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.SystemBundle;
 import org.apache.nifi.provenance.MockProvenanceRepository;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.variable.FileBasedVariableRegistry;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.util.NiFiProperties;
@@ -80,7 +81,8 @@ public class StandardFlowSerializerTest {
         final VariableRegistry variableRegistry = new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths());
 
         final BulletinRepository bulletinRepo = Mockito.mock(BulletinRepository.class);
-        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer, auditService, encryptor, bulletinRepo, variableRegistry);
+        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer,
+            auditService, encryptor, bulletinRepo, variableRegistry, Mockito.mock(FlowRegistryClient.class));
 
         serializer = new StandardFlowSerializer(encryptor);
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
index a28eb34..27e1678 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 
@@ -45,6 +46,9 @@ import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.ProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.remote.RemoteGroupPort;
 
@@ -53,6 +57,7 @@ public class MockProcessGroup implements ProcessGroup {
     private final Map<String, ProcessorNode> processorMap = new HashMap<>();
     private final FlowController flowController;
     private final MutableVariableRegistry variableRegistry = new MutableVariableRegistry(VariableRegistry.ENVIRONMENT_SYSTEM_REGISTRY);
+    private VersionControlInformation versionControlInfo;
 
     public MockProcessGroup(final FlowController flowController) {
         this.flowController = flowController;
@@ -625,4 +630,35 @@ public class MockProcessGroup implements ProcessGroup {
     public Set<ConfiguredComponent> getComponentsAffectedByVariable(String variableName) {
         return Collections.emptySet();
     }
+
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.empty();
+    }
+
+    @Override
+    public void setVersionedComponentId(String versionedComponentId) {
+    }
+
+    @Override
+    public VersionControlInformation getVersionControlInformation() {
+        return versionControlInfo;
+    }
+
+    @Override
+    public void verifyCanUpdate(VersionedFlowSnapshot updatedFlow, boolean verifyConnectionRemoval, boolean verifyNotDirty) {
+    }
+
+    @Override
+    public void synchronizeWithFlowRegistry(FlowRegistryClient flowRegistry) {
+    }
+
+    @Override
+    public void updateFlow(VersionedFlowSnapshot proposedFlow, String componentIdSeed, boolean verifyNotDirty) {
+    }
+
+    @Override
+    public void setVersionControlInformation(VersionControlInformation versionControlInformation, Map<String, String> versionedComponentIds) {
+        this.versionControlInfo = versionControlInformation;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index faa8c0e..84e582c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -16,6 +16,14 @@
  */
 package org.apache.nifi.web;
 
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
 import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
@@ -23,6 +31,11 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.repository.claim.ContentDirection;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.UnknownResourceException;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.web.api.dto.AccessPolicyDTO;
 import org.apache.nifi.web.api.dto.AffectedComponentDTO;
 import org.apache.nifi.web.api.dto.BulletinBoardDTO;
@@ -59,6 +72,7 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.apache.nifi.web.api.dto.UserDTO;
 import org.apache.nifi.web.api.dto.UserGroupDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.dto.action.HistoryDTO;
 import org.apache.nifi.web.api.dto.action.HistoryQueryDTO;
 import org.apache.nifi.web.api.dto.provenance.ProvenanceDTO;
@@ -101,13 +115,9 @@ import org.apache.nifi.web.api.entity.TemplateEntity;
 import org.apache.nifi.web.api.entity.UserEntity;
 import org.apache.nifi.web.api.entity.UserGroupEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryEntity;
-
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
+import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 
 /**
  * Defines the NiFiServiceFacade interface.
@@ -491,6 +501,14 @@ public interface NiFiServiceFacade {
     ProcessorEntity getProcessor(String id);
 
     /**
+     * Gets the Processor transfer object for the specified id, as it is visible to the given user
+     *
+     * @param id Id of the processor to return
+     * @return The Processor transfer object
+     */
+    ProcessorEntity getProcessor(String id, NiFiUser user);
+
+    /**
      * Gets the processor status.
      *
      * @param id id
@@ -1066,6 +1084,16 @@ public interface NiFiServiceFacade {
     RemoteProcessGroupEntity getRemoteProcessGroup(String remoteProcessGroupId);
 
     /**
+     * Gets a remote process group as it is visible to the given user
+     *
+     * @param remoteProcessGroupId The id of the remote process group
+     * @param user the user requesting the action
+     * @return group
+     */
+    RemoteProcessGroupEntity getRemoteProcessGroup(String remoteProcessGroupId, NiFiUser user);
+
+
+    /**
      * Gets all remote process groups in the a given parent group.
      *
      * @param groupId The id of the parent group
@@ -1074,6 +1102,15 @@ public interface NiFiServiceFacade {
     Set<RemoteProcessGroupEntity> getRemoteProcessGroups(String groupId);
 
     /**
+     * Gets all remote process groups in the a given parent group as they are visible to the given user
+     *
+     * @param groupId The id of the parent group
+     * @param user the user making the request
+     * @return group
+     */
+    Set<RemoteProcessGroupEntity> getRemoteProcessGroups(String groupId, NiFiUser user);
+
+    /**
      * Gets the remote process group status.
      *
      * @param id remote process group
@@ -1220,6 +1257,132 @@ public interface NiFiServiceFacade {
      */
     FunnelEntity deleteFunnel(Revision revision, String funnelId);
 
+
+    // ----------------------------------------
+    // Version Control methods
+    // ----------------------------------------
+
+    /**
+     * Returns the Version Control information for the Process Group with the given ID
+     *
+     * @param processGroupId the ID of the Process Group
+     * @return the Version Control information that corresponds to the given Process Group, or <code>null</code> if the
+     *         process group is not under version control
+     */
+    VersionControlInformationEntity getVersionControlInformation(String processGroupId);
+
+
+    /**
+     * Adds the given Versioned Flow to the registry specified by the given ID
+     *
+     * @param registryId the ID of the registry
+     * @param flow the flow to add to the registry
+     * @return a VersionedFlow that is fully populated, including identifiers
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     */
+    VersionedFlow registerVersionedFlow(String registryId, VersionedFlow flow) throws IOException, UnknownResourceException;
+
+    /**
+     * Creates a snapshot of the Process Group with the given identifier, then creates a new Flow entity in the NiFi Registry
+     * and adds the snapshot of the Process Group as the first version of that flow.
+     *
+     * @param groupId the UUID of the Process Group
+     * @param requestEntity the details of the flow to create
+     * @return a VersionControlComponentMappingEntity that contains the information needed to notify a Process Group where it is tracking to and map
+     *         component ID's to their Versioned Component ID's
+     */
+    VersionControlComponentMappingEntity registerFlowWithFlowRegistry(String groupId, VersionedFlowEntity requestEntity);
+
+    /**
+     * Adds the given snapshot to the already existing Versioned Flow, which resides in the given Flow Registry with the given id
+     *
+     * @param registryId the ID of the Flow Registry to persist the snapshot to
+     * @param flow the flow where the snapshot should be persisted
+     * @param snapshot the Snapshot to persist
+     * @param comments about the snapshot
+     * @return the snapshot that represents what was stored in the registry
+     *
+     * @throws IOException if unable to communicate with the Flow Registry
+     */
+    VersionedFlowSnapshot registerVersionedFlowSnapshot(String registryId, VersionedFlow flow, VersionedProcessGroup snapshot, String comments) throws IOException, UnknownResourceException;
+
+    /**
+     * Updates the Version Control Information on the Process Group with the given ID
+     *
+     * @param processGroupRevision the Revision of the Process Group
+     * @param processGroupId the ID of the process group to update
+     * @param versionControlInfo the new Version Control Information
+     * @param versionedComponentMapping a mapping of component ID to Versioned Component ID
+     *
+     * @return a VersionControlInformationEntity that represents the newly updated Version Control information
+     */
+    VersionControlInformationEntity setVersionControlInformation(Revision processGroupRevision, String processGroupId, VersionControlInformationDTO versionControlInfo,
+        Map<String, String> versionedComponentMapping);
+
+
+    /**
+     * Retrieves the Versioned Flow Snapshot for the coordinates provided by the given Version Control Information DTO
+     *
+     * @param versionControlInfo the coordinates of the versioned flow
+     * @return the VersionedFlowSnapshot that corresponds to the given coordinates
+     *
+     * @throws ResourceNotFoundException if the Versioned Flow Snapshot could not be found
+     */
+    VersionedFlowSnapshot getVersionedFlowSnapshot(VersionControlInformationDTO versionControlInfo) throws IOException;
+
+    /**
+     * Determines which components currently exist in the Process Group with the given identifier and calculates which of those components
+     * would be impacted by updating the Process Group to the provided snapshot
+     *
+     * @param processGroupId the ID of the Process Group to update
+     * @param updatedSnapshot the snapshot to update the Process Group to
+     * @param user the user making the request
+     * @return the set of all components that would be affected by updating the Process Group
+     */
+    Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(String processGroupId, VersionedFlowSnapshot updatedSnapshot, NiFiUser user) throws IOException;
+
+    /**
+     * Verifies that the Process Group with the given identifier can be updated to the proposed flow
+     *
+     * @param groupId the ID of the Process Group to update
+     * @param proposedFlow the proposed flow
+     * @param verifyConnectionRemoval whether or not to verify that connections that no longer exist in the proposed flow are eligible for deletion
+     * @param verifyNotDirty whether or not to verify that the Process Group is not 'dirty'. If this value is <code>true</code>,
+     *            and the Process Group has been modified since it was last synchronized with the Flow Registry, then this method will
+     *            throw an IllegalStateException
+     */
+    void verifyCanUpdate(String groupId, VersionedFlowSnapshot proposedFlow, boolean verifyConnectionRemoval, boolean verifyNotDirty);
+
+    /**
+     * Updates the Process group with the given ID to match the new snapshot
+     *
+     * @param revision the revision of the Process Group
+     * @param groupId the ID of the Process Group
+     * @param versionControlInfo the Version Control information
+     * @param snapshot the new snapshot
+     * @param componentIdSeed the seed to use for generating new component ID's
+     * @return the Process Group
+     */
+    ProcessGroupEntity updateProcessGroup(Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
+        boolean verifyNotModified);
+
+    /**
+     * Updates the Process group with the given ID to match the new snapshot
+     *
+     * @param user the user performing the request
+     * @param revision the revision of the Process Group
+     * @param groupId the ID of the Process Group
+     * @param versionControlInfo the Version Control information
+     * @param snapshot the new snapshot
+     * @param componentIdSeed the seed to use for generating new component ID's
+     * @return the Process Group
+     */
+    ProcessGroupEntity updateProcessGroup(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
+        boolean verifyNotModified);
+
+    void setFlowRegistryClient(FlowRegistryClient flowRegistryClient);
+
     // ----------------------------------------
     // Component state methods
     // ----------------------------------------
@@ -1290,6 +1453,7 @@ public interface NiFiServiceFacade {
      */
     void clearReportingTaskState(String reportingTaskId);
 
+
     // ----------------------------------------
     // Label methods
     // ----------------------------------------
@@ -1510,6 +1674,15 @@ public interface NiFiServiceFacade {
     ControllerServiceEntity getControllerService(String controllerServiceId);
 
     /**
+     * Gets the specified controller service as it is visible to the given user
+     *
+     * @param controllerServiceId id
+     * @param user the user making the request
+     * @return service
+     */
+    ControllerServiceEntity getControllerService(String controllerServiceId, NiFiUser user);
+
+    /**
      * Get the descriptor for the specified property of the specified controller service.
      *
      * @param id id

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacadeLock.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacadeLock.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacadeLock.java
index d0230db..f7f3b90 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacadeLock.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacadeLock.java
@@ -138,6 +138,12 @@ public class NiFiServiceFacadeLock {
         return proceedWithReadLock(proceedingJoinPoint);
     }
 
+    @Around("within(org.apache.nifi.web.NiFiServiceFacade+) && "
+        + "execution(* register*(..))")
+    public Object registerLock(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
+        return proceedWithReadLock(proceedingJoinPoint);
+    }
+
 
     private Object proceedWithReadLock(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
         final long beforeLock = System.nanoTime();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 9caabd6..d3a5fd0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,7 +16,32 @@
  */
 package org.apache.nifi.web;
 
-import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
 import org.apache.nifi.action.FlowChangeAction;
@@ -85,6 +110,31 @@ import org.apache.nifi.history.History;
 import org.apache.nifi.history.HistoryQuery;
 import org.apache.nifi.history.PreviousValue;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.flow.ConnectableComponent;
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.RemoteFlowCoordinates;
+import org.apache.nifi.registry.flow.UnknownResourceException;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.VersionedConnection;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.DifferenceType;
+import org.apache.nifi.registry.flow.diff.FlowComparator;
+import org.apache.nifi.registry.flow.diff.FlowComparison;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
+import org.apache.nifi.registry.flow.mapping.InstantiatedConnectableComponent;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteGroupPort;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.reporting.Bulletin;
 import org.apache.nifi.reporting.BulletinQuery;
@@ -140,6 +190,8 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.apache.nifi.web.api.dto.UserDTO;
 import org.apache.nifi.web.api.dto.UserGroupDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 import org.apache.nifi.web.api.dto.action.HistoryDTO;
 import org.apache.nifi.web.api.dto.action.HistoryQueryDTO;
 import org.apache.nifi.web.api.dto.flow.FlowDTO;
@@ -197,6 +249,9 @@ import org.apache.nifi.web.api.entity.UserEntity;
 import org.apache.nifi.web.api.entity.UserGroupEntity;
 import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryEntity;
+import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.dao.AccessPolicyDAO;
 import org.apache.nifi.web.dao.ConnectionDAO;
@@ -224,29 +279,7 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
+import com.google.common.collect.Sets;
 
 /**
  * Implementation of NiFiServiceFacade that performs revision checking.
@@ -285,6 +318,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     // administrative services
     private AuditService auditService;
 
+    // flow registry
+    private FlowRegistryClient flowRegistryClient;
+
     // properties
     private NiFiProperties properties;
     private DtoFactory dtoFactory;
@@ -925,7 +961,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                     @Override
                     public RevisionUpdate<ScheduleComponentsEntity> update() {
                         // schedule the components
-                processGroupDAO.scheduleComponents(processGroupId, state, componentRevisions.keySet());
+                        processGroupDAO.scheduleComponents(processGroupId, state, componentRevisions.keySet());
 
                         // update the revisions
                         final Map<String, Revision> updatedRevisions = new HashMap<>();
@@ -950,7 +986,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public ActivateControllerServicesEntity activateControllerServices(final String processGroupId, final ControllerServiceState state, final Map<String, Revision> serviceRevisions) {
-
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
         return activateControllerServices(user, processGroupId, state, serviceRevisions);
     }
@@ -1010,6 +1045,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entityFactory.createControllerConfigurationEntity(updatedComponent.getComponent(), updateRevision, permissions);
     }
 
+
     @Override
     public NodeDTO updateNode(final NodeDTO nodeDTO) {
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
@@ -2512,9 +2548,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entityFactory.createStatusHistoryEntity(dto, permissions);
     }
 
-    private ProcessorEntity createProcessorEntity(final ProcessorNode processor) {
+    private ProcessorEntity createProcessorEntity(final ProcessorNode processor, final NiFiUser user) {
         final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processor.getIdentifier()));
-        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor, user);
         final ProcessorStatusDTO status = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processor.getIdentifier()));
         final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processor.getIdentifier()));
         final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
@@ -2524,8 +2560,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     @Override
     public Set<ProcessorEntity> getProcessors(final String groupId, final boolean includeDescendants) {
         final Set<ProcessorNode> processors = processorDAO.getProcessors(groupId, includeDescendants);
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
         return processors.stream()
-            .map(processor -> createProcessorEntity(processor))
+            .map(processor -> createProcessorEntity(processor, user))
             .collect(Collectors.toSet());
     }
 
@@ -2582,8 +2619,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public ProcessorEntity getProcessor(final String id) {
+        return getProcessor(id, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public ProcessorEntity getProcessor(final String id, final NiFiUser user) {
         final ProcessorNode processor = processorDAO.getProcessor(id);
-        return createProcessorEntity(processor);
+        return createProcessorEntity(processor, user);
     }
 
     @Override
@@ -3103,9 +3145,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             .collect(Collectors.toSet());
     }
 
-    private RemoteProcessGroupEntity createRemoteGroupEntity(final RemoteProcessGroup rpg) {
+    private RemoteProcessGroupEntity createRemoteGroupEntity(final RemoteProcessGroup rpg, final NiFiUser user) {
         final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(rpg.getIdentifier()));
-        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(rpg);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(rpg, user);
         final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(controllerFacade.getRemoteProcessGroupStatus(rpg.getIdentifier()));
         final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(rpg.getIdentifier()));
         final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
@@ -3114,9 +3156,14 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public Set<RemoteProcessGroupEntity> getRemoteProcessGroups(final String groupId) {
+        return getRemoteProcessGroups(groupId, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public Set<RemoteProcessGroupEntity> getRemoteProcessGroups(final String groupId, final NiFiUser user) {
         final Set<RemoteProcessGroup> rpgs = remoteProcessGroupDAO.getRemoteProcessGroups(groupId);
         return rpgs.stream()
-            .map(rpg -> createRemoteGroupEntity(rpg))
+            .map(rpg -> createRemoteGroupEntity(rpg, user))
             .collect(Collectors.toSet());
     }
 
@@ -3150,8 +3197,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public RemoteProcessGroupEntity getRemoteProcessGroup(final String remoteProcessGroupId) {
+        return getRemoteProcessGroup(remoteProcessGroupId, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public RemoteProcessGroupEntity getRemoteProcessGroup(final String remoteProcessGroupId, final NiFiUser user) {
         final RemoteProcessGroup rpg = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId);
-        return createRemoteGroupEntity(rpg);
+        return createRemoteGroupEntity(rpg, user);
     }
 
     @Override
@@ -3307,8 +3359,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     @Override
     public ControllerServiceEntity getControllerService(final String controllerServiceId) {
+        return getControllerService(controllerServiceId, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public ControllerServiceEntity getControllerService(final String controllerServiceId, final NiFiUser user) {
         final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId);
-        return createControllerServiceEntity(controllerService, Sets.newHashSet(controllerServiceId), NiFiUserUtils.getNiFiUser());
+        return createControllerServiceEntity(controllerService, Sets.newHashSet(controllerServiceId), user);
     }
 
     @Override
@@ -3375,6 +3432,415 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entityFactory.createStatusHistoryEntity(dto, permissions);
     }
 
+    @Override
+    public VersionControlComponentMappingEntity registerFlowWithFlowRegistry(final String groupId, final VersionedFlowEntity requestEntity) {
+        // Create a VersionedProcessGroup snapshot of the flow as it is currently.
+        final InstantiatedVersionedProcessGroup versionedProcessGroup = createFlowSnapshot(groupId);
+
+        final VersionedFlowDTO versionedFlowDto = requestEntity.getVersionedFlow();
+        final String flowId = versionedFlowDto.getFlowId() == null ? UUID.randomUUID().toString() : versionedFlowDto.getFlowId();
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setBucketIdentifier(versionedFlowDto.getBucketId());
+        versionedFlow.setCreatedTimestamp(System.currentTimeMillis());
+        versionedFlow.setDescription(versionedFlowDto.getDescription());
+        versionedFlow.setModifiedTimestamp(versionedFlow.getCreatedTimestamp());
+        versionedFlow.setName(versionedFlowDto.getFlowName());
+        versionedFlow.setIdentifier(flowId);
+
+        // Add the Versioned Flow and first snapshot to the Flow Registry
+        final String registryId = requestEntity.getVersionedFlow().getRegistryId();
+        final VersionedFlowSnapshot registeredSnapshot;
+        final VersionedFlow registeredFlow;
+
+        String action = "create the flow";
+        try {
+            // first, create the flow in the registry, if necessary
+            if (versionedFlowDto.getFlowId() == null) {
+                registeredFlow = registerVersionedFlow(registryId, versionedFlow);
+            } else {
+                registeredFlow = getVersionedFlow(registryId, versionedFlowDto.getBucketId(), versionedFlowDto.getFlowId());
+            }
+
+            action = "add the local flow to the Flow Registry as the first Snapshot";
+
+            // add first snapshot to the flow in the registry
+            final String comments = versionedFlow.getDescription() == null ? "Initial version of flow" : versionedFlow.getDescription();
+            registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, comments);
+        } catch (final UnknownResourceException e) {
+            throw new IllegalArgumentException(e);
+        } catch (final IOException ioe) {
+            // will result in a 500: Internal Server Error
+            throw new RuntimeException("Failed to communicate with Flow Registry when attempting to " + action);
+        }
+
+        // Update the Process Group with the new VersionControlInformation. (Send this to all nodes).
+        final VersionControlInformationDTO vci = new VersionControlInformationDTO();
+        vci.setBucketId(registeredFlow.getBucketIdentifier());
+        vci.setCurrent(true);
+        vci.setFlowId(registeredFlow.getIdentifier());
+        vci.setGroupId(groupId);
+        vci.setModified(false);
+        vci.setRegistryId(registryId);
+        vci.setVersion(registeredSnapshot.getSnapshotMetadata().getVersion());
+
+        final Map<String, String> mapping = dtoFactory.createVersionControlComponentMappingDto(versionedProcessGroup);
+
+        final Revision groupRevision = revisionManager.getRevision(groupId);
+        final RevisionDTO groupRevisionDto = dtoFactory.createRevisionDTO(groupRevision);
+
+        final VersionControlComponentMappingEntity entity = new VersionControlComponentMappingEntity();
+        entity.setVersionControlInformation(vci);
+        entity.setProcessGroupRevision(groupRevisionDto);
+        entity.setVersionControlComponentMapping(mapping);
+        return entity;
+    }
+
+    @Override
+    public VersionControlInformationEntity getVersionControlInformation(final String groupId) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final VersionControlInformation versionControlInfo = processGroup.getVersionControlInformation();
+        if (versionControlInfo == null) {
+            return null;
+        }
+
+        final VersionControlInformationDTO versionControlDto = dtoFactory.createVersionControlInformationDto(versionControlInfo);
+        final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(groupId));
+        return entityFactory.createVersionControlInformationEntity(versionControlDto, groupRevision);
+    }
+
+    private InstantiatedVersionedProcessGroup createFlowSnapshot(final String processGroupId) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId);
+        final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
+        final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient);
+        return versionedGroup;
+    }
+
+    @Override
+    public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) throws IOException, UnknownResourceException {
+        final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
+        if (registry == null) {
+            throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
+        }
+
+        return registry.registerVersionedFlow(flow);
+    }
+
+    private VersionedFlow getVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, UnknownResourceException {
+        final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
+        if (registry == null) {
+            throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
+        }
+
+        return registry.getVersionedFlow(bucketId, flowId);
+    }
+
+    @Override
+    public VersionedFlowSnapshot registerVersionedFlowSnapshot(final String registryId, final VersionedFlow flow,
+            final VersionedProcessGroup snapshot, final String comments) throws IOException, UnknownResourceException {
+        final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
+        if (registry == null) {
+            throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
+        }
+
+        return registry.registerVersionedFlowSnapshot(flow, snapshot, comments);
+    }
+
+    @Override
+    public VersionControlInformationEntity setVersionControlInformation(final Revision revision, final String processGroupId,
+            final VersionControlInformationDTO versionControlInfo, final Map<String, String> versionedComponentMapping) {
+
+        final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
+
+        final RevisionUpdate<VersionControlInformationDTO> snapshot = updateComponent(revision,
+            group,
+            () -> processGroupDAO.updateVersionControlInformation(versionControlInfo, versionedComponentMapping),
+            processGroup -> dtoFactory.createVersionControlInformationDto(processGroup.getVersionControlInformation()));
+
+        return entityFactory.createVersionControlInformationEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()));
+    }
+
+    @Override
+    public void verifyCanUpdate(final String groupId, final VersionedFlowSnapshot proposedFlow, final boolean verifyConnectionRemoval, final boolean verifyNotDirty) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        group.verifyCanUpdate(proposedFlow, verifyConnectionRemoval, verifyNotDirty);
+    }
+
+    @Override
+    public Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(final String processGroupId, final VersionedFlowSnapshot updatedSnapshot, final NiFiUser user) throws IOException {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
+
+        final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
+        final VersionedProcessGroup localContents = mapper.mapProcessGroup(group, flowRegistryClient);
+
+        final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localContents);
+        final ComparableDataFlow proposedFlow = new StandardComparableDataFlow("Proposed Flow", updatedSnapshot.getFlowContents());
+
+        final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow);
+        final FlowComparison comparison = flowComparator.compare();
+
+        final Set<AffectedComponentEntity> affectedComponents = comparison.getDifferences().stream()
+            .filter(difference -> difference.getDifferenceType() != DifferenceType.COMPONENT_ADDED) // components that are added are not components that will be affected in the local flow.
+            .map(difference -> {
+                final VersionedComponent localComponent = difference.getComponentA();
+
+                final String state;
+                switch (localComponent.getComponentType()) {
+                    case CONTROLLER_SERVICE:
+                        final String serviceId = ((InstantiatedVersionedControllerService) localComponent).getInstanceId();
+                        state = controllerServiceDAO.getControllerService(serviceId).getState().name();
+                        break;
+                    case PROCESSOR:
+                        final String processorId = ((InstantiatedVersionedProcessor) localComponent).getInstanceId();
+                        state = processorDAO.getProcessor(processorId).getPhysicalScheduledState().name();
+                        break;
+                    case REMOTE_INPUT_PORT:
+                        final InstantiatedVersionedRemoteGroupPort inputPort = (InstantiatedVersionedRemoteGroupPort) localComponent;
+                        state = remoteProcessGroupDAO.getRemoteProcessGroup(inputPort.getInstanceGroupId()).getInputPort(inputPort.getInstanceId()).getScheduledState().name();
+                        break;
+                    case REMOTE_OUTPUT_PORT:
+                        final InstantiatedVersionedRemoteGroupPort outputPort = (InstantiatedVersionedRemoteGroupPort) localComponent;
+                        state = remoteProcessGroupDAO.getRemoteProcessGroup(outputPort.getInstanceGroupId()).getOutputPort(outputPort.getInstanceId()).getScheduledState().name();
+                        break;
+                    default:
+                        state = null;
+                        break;
+                }
+
+                return createAffectedComponentEntity((InstantiatedVersionedComponent) localComponent, localComponent.getComponentType().name(), state, user);
+            })
+            .collect(Collectors.toCollection(HashSet::new));
+
+        for (final FlowDifference difference : comparison.getDifferences()) {
+            VersionedComponent component = difference.getComponentA();
+            if (component == null) {
+                component = difference.getComponentB();
+            }
+
+            if (component.getComponentType() == org.apache.nifi.registry.flow.ComponentType.CONNECTION) {
+                final VersionedConnection connection = (VersionedConnection) component;
+
+                final ConnectableComponent source = connection.getSource();
+                final ConnectableComponent destination = connection.getDestination();
+
+                affectedComponents.add(createAffectedComponentEntity((InstantiatedConnectableComponent) source, user));
+                affectedComponents.add(createAffectedComponentEntity((InstantiatedConnectableComponent) destination, user));
+            }
+        }
+
+        return affectedComponents;
+    }
+
+    private String getComponentState(final InstantiatedConnectableComponent localComponent) {
+        final String componentId = localComponent.getInstanceId();
+        final String groupId = localComponent.getInstanceGroupId();
+
+        switch (localComponent.getType()) {
+            case PROCESSOR:
+                return processorDAO.getProcessor(componentId).getPhysicalScheduledState().name();
+            case REMOTE_INPUT_PORT:
+                return remoteProcessGroupDAO.getRemoteProcessGroup(groupId).getInputPort(componentId).getScheduledState().name();
+            case REMOTE_OUTPUT_PORT:
+                return remoteProcessGroupDAO.getRemoteProcessGroup(groupId).getOutputPort(componentId).getScheduledState().name();
+            default:
+                return null;
+        }
+    }
+
+    private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedVersionedComponent instance, final String componentTypeName, final String componentState, final NiFiUser user) {
+        final AffectedComponentEntity entity = new AffectedComponentEntity();
+        entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(instance.getInstanceId())));
+        entity.setId(instance.getInstanceId());
+
+        final Authorizable authorizable = getAuthorizable(componentTypeName, instance);
+        final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable, user);
+        entity.setPermissions(permissionsDto);
+
+        final AffectedComponentDTO dto = new AffectedComponentDTO();
+        dto.setId(instance.getInstanceId());
+        dto.setReferenceType(componentTypeName);
+        dto.setProcessGroupId(instance.getInstanceGroupId());
+        dto.setState(componentState);
+
+        entity.setComponent(dto);
+        return entity;
+    }
+
+    private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedConnectableComponent instance, final NiFiUser user) {
+        final AffectedComponentEntity entity = new AffectedComponentEntity();
+        entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(instance.getInstanceId())));
+        entity.setId(instance.getInstanceId());
+
+        final String componentTypeName = instance.getType().name();
+        final Authorizable authorizable = getAuthorizable(componentTypeName, instance);
+        final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable, user);
+        entity.setPermissions(permissionsDto);
+
+        final AffectedComponentDTO dto = new AffectedComponentDTO();
+        dto.setId(instance.getInstanceId());
+        dto.setReferenceType(componentTypeName);
+        dto.setProcessGroupId(instance.getInstanceGroupId());
+        dto.setState(getComponentState(instance));
+
+        entity.setComponent(dto);
+        return entity;
+    }
+
+    private Authorizable getAuthorizable(final String componentTypeName, final InstantiatedVersionedComponent versionedComponent) {
+        final String componentId = versionedComponent.getInstanceId();
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.CONTROLLER_SERVICE.name())) {
+            return authorizableLookup.getControllerService(componentId).getAuthorizable();
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.CONNECTION.name())) {
+            return authorizableLookup.getConnection(componentId).getAuthorizable();
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.FUNNEL.name())) {
+            return authorizableLookup.getFunnel(componentId);
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.INPUT_PORT.name())) {
+            return authorizableLookup.getInputPort(componentId);
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.OUTPUT_PORT.name())) {
+            return authorizableLookup.getOutputPort(componentId);
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.LABEL.name())) {
+            return authorizableLookup.getLabel(componentId);
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.PROCESS_GROUP.name())) {
+            return authorizableLookup.getProcessGroup(componentId).getAuthorizable();
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.PROCESSOR.name())) {
+            return authorizableLookup.getProcessor(componentId).getAuthorizable();
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_INPUT_PORT.name())) {
+            return authorizableLookup.getRemoteProcessGroup(versionedComponent.getInstanceGroupId());
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_OUTPUT_PORT.name())) {
+            return authorizableLookup.getRemoteProcessGroup(versionedComponent.getInstanceGroupId());
+        }
+
+        if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_PROCESS_GROUP.name())) {
+            return authorizableLookup.getRemoteProcessGroup(versionedComponent.getInstanceGroupId());
+        }
+
+        return null;
+    }
+
+    @Override
+    public VersionedFlowSnapshot getVersionedFlowSnapshot(final VersionControlInformationDTO versionControlInfo) throws IOException {
+        final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(versionControlInfo.getRegistryId());
+        if (flowRegistry == null) {
+            throw new ResourceNotFoundException("Could not find any Flow Registry registered with identifier " + versionControlInfo.getRegistryId());
+        }
+
+        final VersionedFlowSnapshot snapshot;
+        try {
+            snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion());
+        } catch (final UnknownResourceException e) {
+            throw new IllegalArgumentException("The Flow Registry with ID " + versionControlInfo.getRegistryId() + " reports that no Flow exists with Bucket "
+                + versionControlInfo.getBucketId() + ", Flow " + versionControlInfo.getFlowId() + ", Version " + versionControlInfo.getVersion());
+        }
+
+        // If this Flow has a reference to a remote flow, we need to pull that remote flow as well
+        populateVersionedChildFlows(snapshot);
+
+        return snapshot;
+    }
+
+    private void populateVersionedChildFlows(final VersionedFlowSnapshot snapshot) throws IOException {
+        final VersionedProcessGroup group = snapshot.getFlowContents();
+
+        for (final VersionedProcessGroup child : group.getProcessGroups()) {
+            populateVersionedFlows(child);
+        }
+    }
+
+    private void populateVersionedFlows(final VersionedProcessGroup group) throws IOException {
+        final RemoteFlowCoordinates remoteCoordinates = group.getRemoteFlowCoordinates();
+
+        if (remoteCoordinates != null) {
+            final String registryUrl = remoteCoordinates.getRegistryUrl();
+            final String registryId = flowRegistryClient.getFlowRegistryId(registryUrl);
+            if (registryId == null) {
+                throw new IllegalArgumentException("Process Group with ID " + group.getIdentifier() + " is under Version Control, referencing a Flow Registry at URL [" + registryUrl
+                    + "], but no Flow Registry is currently registered for that URL.");
+            }
+
+            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
+
+            final VersionedFlowSnapshot childSnapshot;
+            try {
+                childSnapshot = flowRegistry.getFlowContents(remoteCoordinates.getBucketId(), remoteCoordinates.getFlowId(), remoteCoordinates.getVersion());
+            } catch (final UnknownResourceException e) {
+                throw new IllegalArgumentException("The Flow Registry with ID " + registryId + " reports that no Flow exists with Bucket "
+                    + remoteCoordinates.getBucketId() + ", Flow " + remoteCoordinates.getFlowId() + ", Version " + remoteCoordinates.getVersion());
+            }
+
+            final VersionedProcessGroup fetchedGroup = childSnapshot.getFlowContents();
+            group.setComments(fetchedGroup.getComments());
+            group.setPosition(fetchedGroup.getPosition());
+            group.setName(fetchedGroup.getName());
+            group.setVariables(fetchedGroup.getVariables());
+
+            group.setConnections(new LinkedHashSet<>(fetchedGroup.getConnections()));
+            group.setControllerServices(new LinkedHashSet<>(fetchedGroup.getControllerServices()));
+            group.setFunnels(new LinkedHashSet<>(fetchedGroup.getFunnels()));
+            group.setInputPorts(new LinkedHashSet<>(fetchedGroup.getInputPorts()));
+            group.setLabels(new LinkedHashSet<>(fetchedGroup.getLabels()));
+            group.setOutputPorts(new LinkedHashSet<>(fetchedGroup.getOutputPorts()));
+            group.setProcessGroups(new LinkedHashSet<>(fetchedGroup.getProcessGroups()));
+            group.setProcessors(new LinkedHashSet<>(fetchedGroup.getProcessors()));
+            group.setRemoteProcessGroups(new LinkedHashSet<>(fetchedGroup.getRemoteProcessGroups()));
+        }
+
+        for (final VersionedProcessGroup child : group.getProcessGroups()) {
+            populateVersionedFlows(child);
+        }
+    }
+
+
+    @Override
+    public ProcessGroupEntity updateProcessGroup(final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
+        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified) {
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        return updateProcessGroup(user, revision, groupId, versionControlInfo, proposedFlowSnapshot, componentIdSeed, verifyNotModified);
+    }
+
+    @Override
+    public ProcessGroupEntity updateProcessGroup(final NiFiUser user, final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
+        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified) {
+
+        final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(groupId);
+        final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(user, revision,
+            processGroupNode,
+            () -> processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified),
+            processGroup -> dtoFactory.createProcessGroupDto(processGroup));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);
+        final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
+        final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(processGroupNode.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroupNode.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createProcessGroupEntity(snapshot.getComponent(), updatedRevision, permissions, status, bulletinEntities);
+    }
+
+
+    @Override
+    public void setFlowRegistryClient(final FlowRegistryClient client) {
+        this.flowRegistryClient = client;
+    }
+
     private AuthorizationResult authorizeAction(final Action action) {
         final String sourceId = action.getSourceId();
         final Component type = action.getSourceType();

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index 7118e01..531823a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -16,40 +16,8 @@
  */
 package org.apache.nifi.web.api;
 
-import static javax.ws.rs.core.Response.Status.NOT_FOUND;
-import static org.apache.commons.lang3.StringUtils.isEmpty;
-import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_NAME;
-import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_VALUE;
-
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.BiFunction;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.ws.rs.core.CacheControl;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriBuilderException;
-import javax.ws.rs.core.UriInfo;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeAccess;
@@ -69,6 +37,7 @@ import org.apache.nifi.cluster.manager.exception.IllegalClusterStateException;
 import org.apache.nifi.cluster.manager.exception.UnknownNodeException;
 import org.apache.nifi.cluster.protocol.NodeIdentifier;
 import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.framework.security.util.SslContextFactory;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
 import org.apache.nifi.remote.VersionNegotiator;
 import org.apache.nifi.remote.exception.BadRequestException;
@@ -77,6 +46,7 @@ import org.apache.nifi.remote.exception.NotAuthorizedException;
 import org.apache.nifi.remote.protocol.ResponseCode;
 import org.apache.nifi.remote.protocol.http.HttpHeaders;
 import org.apache.nifi.util.ComponentIdGenerator;
+import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.Revision;
@@ -87,9 +57,45 @@ import org.apache.nifi.web.api.entity.TransactionResultEntity;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.util.CacheKey;
 import org.apache.nifi.web.util.WebUtils;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriBuilderException;
+import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_NAME;
+import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_VALUE;
+
 /**
  * Base class for controllers.
  */
@@ -221,6 +227,16 @@ public abstract class ApplicationResource {
         return Optional.of(idGenerationSeed);
     }
 
+    protected Client createJerseyClient() {
+        final NiFiProperties properties = getProperties();
+        final ClientConfig clientConfig = new ClientConfig();
+        final int connectionTimeout = (int) FormatUtils.getTimeDuration(properties.getClusterNodeConnectionTimeout(), TimeUnit.MILLISECONDS);
+        final int readTimeout = (int) FormatUtils.getTimeDuration(properties.getClusterNodeReadTimeout(), TimeUnit.MILLISECONDS);
+        clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout);
+        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectionTimeout);
+        clientConfig.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.TRUE);
+        return WebUtils.createClient(clientConfig, SslContextFactory.createSslContext(properties));
+    }
 
     /**
      * Generates an Ok response with no content.

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index c9aea4f..38e8891 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -595,6 +595,24 @@ public class FlowResource extends ApplicationResource {
                             componentIds.add(outputPort.getIdentifier());
                         });
 
+                // ensure authorized for each remote input port we will attempt to schedule
+                group.findAllRemoteProcessGroups().stream()
+                    .flatMap(rpg -> rpg.getInputPorts().stream())
+                    .filter(ScheduledState.RUNNING.equals(state) ? ProcessGroup.SCHEDULABLE_PORTS : ProcessGroup.UNSCHEDULABLE_PORTS)
+                    .filter(port -> port.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
+                    .forEach(port -> {
+                        componentIds.add(port.getIdentifier());
+                    });
+
+                // ensure authorized for each remote output port we will attempt to schedule
+                group.findAllRemoteProcessGroups().stream()
+                    .flatMap(rpg -> rpg.getOutputPorts().stream())
+                    .filter(ScheduledState.RUNNING.equals(state) ? ProcessGroup.SCHEDULABLE_PORTS : ProcessGroup.UNSCHEDULABLE_PORTS)
+                    .filter(port -> port.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
+                    .forEach(port -> {
+                        componentIds.add(port.getIdentifier());
+                    });
+
                 return componentIds;
             });
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index b866677..ebed0ad 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -763,7 +763,6 @@ public class ProcessGroupResource extends ApplicationResource {
     private void updateVariableRegistryReplicated(final String groupId, final URI originalUri, final Collection<AffectedComponentDTO> affectedProcessors,
                                                   final Collection<AffectedComponentDTO> affectedServices, final VariableRegistryUpdateRequest updateRequest,
                                                   final VariableRegistryEntity requestEntity) throws InterruptedException, IOException {
-
         final Pause pause = createPause(updateRequest);
 
         // stop processors
@@ -805,8 +804,6 @@ public class ProcessGroupResource extends ApplicationResource {
             logger.info("In order to update Variable Registry for Process Group with ID {}, no Processors are affected.", groupId);
             updateRequest.getStartProcessorsStep().setComplete(true);
         }
-
-        updateRequest.setComplete(true);
     }
 
     /**
@@ -1414,6 +1411,7 @@ public class ProcessGroupResource extends ApplicationResource {
      * @param <T> type of class
      * @return the response entity
      */
+    @SuppressWarnings("unchecked")
     private <T> T getResponseEntity(final NodeResponse nodeResponse, final Class<T> clazz) {
         T entity = (T) nodeResponse.getUpdatedEntity();
         if (entity == null) {


[49/50] nifi git commit: NIFI-4436: - Code clean up. - Backing out incomplete fix for variable registry two phase commit to address in separate JIRA.

Posted by bb...@apache.org.
NIFI-4436:
- Code clean up.
- Backing out incomplete fix for variable registry two phase commit to address in separate JIRA.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/fd18eeb8
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/fd18eeb8
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/fd18eeb8

Branch: refs/heads/master
Commit: fd18eeb84e976c7ac77a92ee5e36764697e63abb
Parents: 118667a
Author: Matt Gilman <ma...@gmail.com>
Authored: Wed Jan 3 10:46:04 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:57 2018 -0500

----------------------------------------------------------------------
 .../web/api/dto/ControllerConfigurationDTO.java |  1 -
 .../nifi/web/api/dto/FlowConfigurationDTO.java  |  1 -
 .../api/entity/CreateActiveRequestEntity.java   |  2 +-
 .../StartVersionControlRequestEntity.java       |  2 +-
 .../web/api/entity/VersionedFlowEntity.java     |  2 +-
 .../api/entity/VersionedFlowSnapshotEntity.java |  2 +-
 .../VersionedFlowSnapshotMetadataEntity.java    |  2 +-
 .../nifi/web/api/ApplicationResource.java       | 16 -----
 .../nifi/web/api/ProcessGroupResource.java      | 66 ++++++++++----------
 9 files changed, 37 insertions(+), 57 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
index 597d700..61b037f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerConfigurationDTO.java
@@ -28,7 +28,6 @@ public class ControllerConfigurationDTO {
 
     private Integer maxTimerDrivenThreadCount;
     private Integer maxEventDrivenThreadCount;
-    private String registryUrl;
 
     /**
      * @return maximum number of timer driven threads this NiFi has available

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
index d7a0c83..657d760 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
@@ -127,5 +127,4 @@ public class FlowConfigurationDTO {
     public void setTimeOffset(Integer timeOffset) {
         this.timeOffset = timeOffset;
     }
-
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
index 63a07a2..8074e00 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CreateActiveRequestEntity.java
@@ -21,7 +21,7 @@ import io.swagger.annotations.ApiModelProperty;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-@XmlRootElement(name = "createActiveRequest")
+@XmlRootElement(name = "createActiveRequestEntity")
 public class CreateActiveRequestEntity extends Entity {
     private String processGroupId;
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
index 0876140..b5b943d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/StartVersionControlRequestEntity.java
@@ -23,7 +23,7 @@ import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-@XmlRootElement(name = "versionedFlow")
+@XmlRootElement(name = "startVersionControlRequestEntity")
 public class StartVersionControlRequestEntity extends Entity {
     private VersionedFlowDTO versionedFlow;
     private RevisionDTO processGroupRevision;

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
index 4a43826..26772ad 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowEntity.java
@@ -22,7 +22,7 @@ import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-@XmlRootElement(name = "versionedFlow")
+@XmlRootElement(name = "versionedFlowEntity")
 public class VersionedFlowEntity extends Entity {
     private VersionedFlowDTO versionedFlow;
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
index 2faf791..8929fa7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
@@ -23,7 +23,7 @@ import org.apache.nifi.web.api.dto.RevisionDTO;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-@XmlRootElement(name = "versionedFlowSnapshot")
+@XmlRootElement(name = "versionedFlowSnapshotEntity")
 public class VersionedFlowSnapshotEntity extends Entity {
     private VersionedFlowSnapshot versionedFlowSnapshot;
     private RevisionDTO processGroupRevision;

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
index 29b57cc..face25c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotMetadataEntity.java
@@ -22,7 +22,7 @@ import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-@XmlRootElement(name = "versionedFlowSnapshotMetadata")
+@XmlRootElement(name = "versionedFlowSnapshotMetadataEntity")
 public class VersionedFlowSnapshotMetadataEntity extends Entity {
     private VersionedFlowSnapshotMetadata versionedFlowSnapshotMetadata;
     private String registryId;

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index 531823a..7e19d98 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -37,7 +37,6 @@ import org.apache.nifi.cluster.manager.exception.IllegalClusterStateException;
 import org.apache.nifi.cluster.manager.exception.UnknownNodeException;
 import org.apache.nifi.cluster.protocol.NodeIdentifier;
 import org.apache.nifi.controller.FlowController;
-import org.apache.nifi.framework.security.util.SslContextFactory;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
 import org.apache.nifi.remote.VersionNegotiator;
 import org.apache.nifi.remote.exception.BadRequestException;
@@ -46,7 +45,6 @@ import org.apache.nifi.remote.exception.NotAuthorizedException;
 import org.apache.nifi.remote.protocol.ResponseCode;
 import org.apache.nifi.remote.protocol.http.HttpHeaders;
 import org.apache.nifi.util.ComponentIdGenerator;
-import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.Revision;
@@ -57,14 +55,11 @@ import org.apache.nifi.web.api.entity.TransactionResultEntity;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.util.CacheKey;
 import org.apache.nifi.web.util.WebUtils;
-import org.glassfish.jersey.client.ClientConfig;
-import org.glassfish.jersey.client.ClientProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.ws.rs.client.Client;
 import javax.ws.rs.core.CacheControl;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
@@ -227,17 +222,6 @@ public abstract class ApplicationResource {
         return Optional.of(idGenerationSeed);
     }
 
-    protected Client createJerseyClient() {
-        final NiFiProperties properties = getProperties();
-        final ClientConfig clientConfig = new ClientConfig();
-        final int connectionTimeout = (int) FormatUtils.getTimeDuration(properties.getClusterNodeConnectionTimeout(), TimeUnit.MILLISECONDS);
-        final int readTimeout = (int) FormatUtils.getTimeDuration(properties.getClusterNodeReadTimeout(), TimeUnit.MILLISECONDS);
-        clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout);
-        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectionTimeout);
-        clientConfig.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.TRUE);
-        return WebUtils.createClient(clientConfig, SslContextFactory.createSslContext(properties));
-    }
-
     /**
      * Generates an Ok response with no content.
      *

http://git-wip-us.apache.org/repos/asf/nifi/blob/fd18eeb8/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index a2c16ed..96e1e08 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -16,32 +16,12 @@
  */
 package org.apache.nifi.web.api;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.stream.XMLStreamReader;
-
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeAccess;
@@ -132,6 +112,31 @@ import org.slf4j.LoggerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
@@ -157,13 +162,6 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
-
 /**
  * RESTful endpoint for managing a Group.
  */
@@ -622,7 +620,7 @@ public class ProcessGroupResource extends ApplicationResource {
         final VariableRegistryDTO requestRegistryDto = requestVariableRegistryEntity.getVariableRegistry();
         if (!groupId.equals(requestRegistryDto.getProcessGroupId())) {
             throw new IllegalArgumentException(String.format("The process group id (%s) in the request body does "
-                + "not equal the process group id of the requested resource (%s).", requestRegistryDto.getProcessGroupId(), groupId));
+                    + "not equal the process group id of the requested resource (%s).", registryDto.getProcessGroupId(), groupId));
         }
 
         if (isReplicateRequest()) {


[13/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
new file mode 100644
index 0000000..41b98ed
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardVersionControlInformation.java
@@ -0,0 +1,89 @@
+/*
+ * 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.registry.flow;
+
+import java.util.Optional;
+
+public class StandardVersionControlInformation implements VersionControlInformation {
+
+    private final String registryIdentifier;
+    private final String bucketIdentifier;
+    private final String flowIdentifier;
+    private final int version;
+    private volatile VersionedProcessGroup flowSnapshot;
+    private volatile Boolean modified = null;
+    private volatile Boolean current = null;
+
+    public StandardVersionControlInformation(final String registryId, final String bucketId, final String flowId, final int version,
+        final VersionedProcessGroup snapshot, final Boolean modified, final Boolean current) {
+        this.registryIdentifier = registryId;
+        this.bucketIdentifier = bucketId;
+        this.flowIdentifier = flowId;
+        this.version = version;
+        this.flowSnapshot = snapshot;
+        this.modified = modified;
+        this.current = current;
+    }
+
+    @Override
+    public String getRegistryIdentifier() {
+        return registryIdentifier;
+    }
+
+    @Override
+    public String getBucketIdentifier() {
+        return bucketIdentifier;
+    }
+
+    @Override
+    public String getFlowIdentifier() {
+        return flowIdentifier;
+    }
+
+    @Override
+    public int getVersion() {
+        return version;
+    }
+
+    @Override
+    public Optional<Boolean> getModified() {
+        return Optional.ofNullable(modified);
+    }
+
+    @Override
+    public Optional<Boolean> getCurrent() {
+        return Optional.ofNullable(current);
+    }
+
+    @Override
+    public VersionedProcessGroup getFlowSnapshot() {
+        return flowSnapshot;
+    }
+
+    public void setModified(final boolean modified) {
+        this.modified = modified;
+    }
+
+    public void setCurrent(final boolean current) {
+        this.current = current;
+    }
+
+    public void setFlowSnapshot(final VersionedProcessGroup flowSnapshot) {
+        this.flowSnapshot = flowSnapshot;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedConnectableComponent.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedConnectableComponent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedConnectableComponent.java
new file mode 100644
index 0000000..26ad300
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedConnectableComponent.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.ConnectableComponent;
+
+public class InstantiatedConnectableComponent extends ConnectableComponent implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedConnectableComponent(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedComponent.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedComponent.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedComponent.java
new file mode 100644
index 0000000..15c620a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedComponent.java
@@ -0,0 +1,24 @@
+/*
+ * 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.registry.flow.mapping;
+
+public interface InstantiatedVersionedComponent {
+    String getInstanceId();
+
+    String getInstanceGroupId();
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedConnection.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedConnection.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedConnection.java
new file mode 100644
index 0000000..d18733a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedConnection.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedConnection;
+
+public class InstantiatedVersionedConnection extends VersionedConnection implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedConnection(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedControllerService.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedControllerService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedControllerService.java
new file mode 100644
index 0000000..0617cd5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedControllerService.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedControllerService;
+
+public class InstantiatedVersionedControllerService extends VersionedControllerService implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedControllerService(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedFunnel.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedFunnel.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedFunnel.java
new file mode 100644
index 0000000..6b1f230
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedFunnel.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedFunnel;
+
+public class InstantiatedVersionedFunnel extends VersionedFunnel implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedFunnel(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedLabel.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedLabel.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedLabel.java
new file mode 100644
index 0000000..1c061c8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedLabel.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedLabel;
+
+public class InstantiatedVersionedLabel extends VersionedLabel implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedLabel(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedPort.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedPort.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedPort.java
new file mode 100644
index 0000000..b3ddb40
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedPort.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedPort;
+
+public class InstantiatedVersionedPort extends VersionedPort implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedPort(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessGroup.java
new file mode 100644
index 0000000..a669220
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessGroup.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+
+public class InstantiatedVersionedProcessGroup extends VersionedProcessGroup implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedProcessGroup(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessor.java
new file mode 100644
index 0000000..2763e9d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedProcessor.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedProcessor;
+
+public class InstantiatedVersionedProcessor extends VersionedProcessor implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedProcessor(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteGroupPort.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteGroupPort.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteGroupPort.java
new file mode 100644
index 0000000..27805fa
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteGroupPort.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
+
+public class InstantiatedVersionedRemoteGroupPort extends VersionedRemoteGroupPort implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String remoteGroupId;
+
+    public InstantiatedVersionedRemoteGroupPort(final String instanceId, final String instanceRemoteGroupId) {
+        this.instanceId = instanceId;
+        this.remoteGroupId = instanceRemoteGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return remoteGroupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteProcessGroup.java
new file mode 100644
index 0000000..57816ec
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/InstantiatedVersionedRemoteProcessGroup.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
+
+public class InstantiatedVersionedRemoteProcessGroup extends VersionedRemoteProcessGroup implements InstantiatedVersionedComponent {
+    private final String instanceId;
+    private final String groupId;
+
+    public InstantiatedVersionedRemoteProcessGroup(final String instanceId, final String instanceGroupId) {
+        this.instanceId = instanceId;
+        this.groupId = instanceGroupId;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public String getInstanceGroupId() {
+        return groupId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
new file mode 100644
index 0000000..c3c1037
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
@@ -0,0 +1,327 @@
+/*
+ * 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.registry.flow.mapping;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.apache.nifi.registry.flow.BatchSize;
+import org.apache.nifi.registry.flow.Bundle;
+import org.apache.nifi.registry.flow.ComponentType;
+import org.apache.nifi.registry.flow.ConnectableComponent;
+import org.apache.nifi.registry.flow.ConnectableComponentType;
+import org.apache.nifi.registry.flow.ControllerServiceAPI;
+import org.apache.nifi.registry.flow.PortType;
+import org.apache.nifi.registry.flow.Position;
+import org.apache.nifi.registry.flow.VersionedConnection;
+import org.apache.nifi.registry.flow.VersionedControllerService;
+import org.apache.nifi.registry.flow.VersionedFunnel;
+import org.apache.nifi.registry.flow.VersionedLabel;
+import org.apache.nifi.registry.flow.VersionedPort;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
+import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
+import org.apache.nifi.web.api.dto.BatchSettingsDTO;
+import org.apache.nifi.web.api.dto.BundleDTO;
+import org.apache.nifi.web.api.dto.ConnectableDTO;
+import org.apache.nifi.web.api.dto.ConnectionDTO;
+import org.apache.nifi.web.api.dto.ControllerServiceApiDTO;
+import org.apache.nifi.web.api.dto.ControllerServiceDTO;
+import org.apache.nifi.web.api.dto.FlowSnippetDTO;
+import org.apache.nifi.web.api.dto.FunnelDTO;
+import org.apache.nifi.web.api.dto.LabelDTO;
+import org.apache.nifi.web.api.dto.PortDTO;
+import org.apache.nifi.web.api.dto.PositionDTO;
+import org.apache.nifi.web.api.dto.ProcessGroupDTO;
+import org.apache.nifi.web.api.dto.ProcessorConfigDTO;
+import org.apache.nifi.web.api.dto.ProcessorDTO;
+import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
+import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
+
+
+public class NiFiRegistryDtoMapper {
+    // We need to keep a mapping of component id to versionedComponentId as we transform these objects. This way, when
+    // we call #mapConnectable, instead of generating a new UUID for the ConnectableComponent, we can lookup the 'versioned'
+    // identifier based on the comopnent's actual id. We do connections last, so that all components will already have been
+    // created before attempting to create the connection, where the ConnectableDTO is converted.
+    private Map<String, String> versionedComponentIds = new HashMap<>();
+
+    public VersionedProcessGroup mapProcessGroup(final ProcessGroupDTO dto) {
+        versionedComponentIds.clear();
+        return mapGroup(dto);
+    }
+
+    private VersionedProcessGroup mapGroup(final ProcessGroupDTO dto) {
+        final VersionedProcessGroup versionedGroup = new VersionedProcessGroup();
+        versionedGroup.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        versionedGroup.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        versionedGroup.setName(dto.getName());
+        versionedGroup.setComments(dto.getComments());
+        versionedGroup.setPosition(mapPosition(dto.getPosition()));
+
+        final FlowSnippetDTO contents = dto.getContents();
+
+        versionedGroup.setControllerServices(contents.getControllerServices().stream()
+            .map(this::mapControllerService)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setFunnels(contents.getFunnels().stream()
+            .map(this::mapFunnel)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setInputPorts(contents.getInputPorts().stream()
+            .map(this::mapPort)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setOutputPorts(contents.getOutputPorts().stream()
+            .map(this::mapPort)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setLabels(contents.getLabels().stream()
+            .map(this::mapLabel)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setProcessors(contents.getProcessors().stream()
+            .map(this::mapProcessor)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setRemoteProcessGroups(contents.getRemoteProcessGroups().stream()
+            .map(this::mapRemoteProcessGroup)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setProcessGroups(contents.getProcessGroups().stream()
+            .map(this::mapGroup)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setConnections(contents.getConnections().stream()
+            .map(this::mapConnection)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        return versionedGroup;
+    }
+
+    private String getId(final String currentVersionedId, final String componentId) {
+        final String versionedId;
+        if (currentVersionedId == null) {
+            versionedId = UUID.nameUUIDFromBytes(componentId.getBytes(StandardCharsets.UTF_8)).toString();
+        } else {
+            versionedId = currentVersionedId;
+        }
+
+        versionedComponentIds.put(componentId, versionedId);
+        return versionedId;
+    }
+
+    private String getGroupId(final String groupId) {
+        return versionedComponentIds.get(groupId);
+    }
+
+    public VersionedConnection mapConnection(final ConnectionDTO dto) {
+        final VersionedConnection connection = new VersionedConnection();
+        connection.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        connection.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        connection.setName(dto.getName());
+        connection.setBackPressureDataSizeThreshold(dto.getBackPressureDataSizeThreshold());
+        connection.setBackPressureObjectThreshold(dto.getBackPressureObjectThreshold());
+        connection.setFlowFileExpiration(dto.getFlowFileExpiration());
+        connection.setLabelIndex(dto.getLabelIndex());
+        connection.setPosition(mapPosition(dto.getPosition()));
+        connection.setPrioritizers(dto.getPrioritizers());
+        connection.setSelectedRelationships(dto.getSelectedRelationships());
+        connection.setzIndex(dto.getzIndex());
+
+        connection.setBends(dto.getBends().stream()
+            .map(this::mapPosition)
+            .collect(Collectors.toList()));
+
+        connection.setSource(mapConnectable(dto.getSource()));
+        connection.setDestination(mapConnectable(dto.getDestination()));
+
+        return connection;
+    }
+
+    public ConnectableComponent mapConnectable(final ConnectableDTO dto) {
+        final ConnectableComponent component = new ConnectableComponent();
+
+        final String versionedId = dto.getVersionedComponentId();
+        if (versionedId == null) {
+            final String resolved = versionedComponentIds.get(dto.getId());
+            if (resolved == null) {
+                throw new IllegalArgumentException("Unable to map Connectable Component with identifier " + dto.getId() + " to any version-controlled component");
+            }
+
+            component.setId(resolved);
+        } else {
+            component.setId(versionedId);
+        }
+
+        component.setComments(dto.getComments());
+        component.setGroupId(dto.getGroupId());
+        component.setName(dto.getName());
+        component.setType(ConnectableComponentType.valueOf(dto.getType()));
+        return component;
+    }
+
+    public VersionedControllerService mapControllerService(final ControllerServiceDTO dto) {
+        final VersionedControllerService service = new VersionedControllerService();
+        service.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        service.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        service.setName(dto.getName());
+        service.setAnnotationData(dto.getAnnotationData());
+        service.setBundle(mapBundle(dto.getBundle()));
+        service.setComments(dto.getComments());
+        service.setControllerServiceApis(dto.getControllerServiceApis().stream()
+            .map(this::mapControllerServiceApi)
+            .collect(Collectors.toList()));
+        service.setProperties(dto.getProperties());
+        service.setType(dto.getType());
+        return null;
+    }
+
+    private Bundle mapBundle(final BundleDTO dto) {
+        final Bundle bundle = new Bundle();
+        bundle.setGroup(dto.getGroup());
+        bundle.setArtifact(dto.getArtifact());
+        bundle.setVersion(dto.getVersion());
+        return bundle;
+    }
+
+    private ControllerServiceAPI mapControllerServiceApi(final ControllerServiceApiDTO dto) {
+        final ControllerServiceAPI api = new ControllerServiceAPI();
+        api.setBundle(mapBundle(dto.getBundle()));
+        api.setType(dto.getType());
+        return api;
+    }
+
+    public VersionedFunnel mapFunnel(final FunnelDTO dto) {
+        final VersionedFunnel funnel = new VersionedFunnel();
+        funnel.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        funnel.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        funnel.setPosition(mapPosition(dto.getPosition()));
+        return funnel;
+    }
+
+    public VersionedLabel mapLabel(final LabelDTO dto) {
+        final VersionedLabel label = new VersionedLabel();
+        label.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        label.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        label.setHeight(dto.getHeight());
+        label.setWidth(dto.getWidth());
+        label.setLabel(dto.getLabel());
+        label.setPosition(mapPosition(dto.getPosition()));
+        label.setStyle(dto.getStyle());
+        return label;
+    }
+
+    public VersionedPort mapPort(final PortDTO dto) {
+        final VersionedPort port = new VersionedPort();
+        port.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        port.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        port.setComments(dto.getComments());
+        port.setConcurrentlySchedulableTaskCount(dto.getConcurrentlySchedulableTaskCount());
+        port.setName(dto.getName());
+        port.setPosition(mapPosition(dto.getPosition()));
+        port.setType(PortType.valueOf(dto.getType()));
+        return port;
+    }
+
+    public Position mapPosition(final PositionDTO dto) {
+        final Position position = new Position();
+        position.setX(dto.getX());
+        position.setY(dto.getY());
+        return position;
+    }
+
+    public VersionedProcessor mapProcessor(final ProcessorDTO dto) {
+        final ProcessorConfigDTO config = dto.getConfig();
+
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        processor.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        processor.setType(dto.getType());
+        processor.setAnnotationData(config.getAnnotationData());
+        processor.setAutoTerminatedRelationships(config.getAutoTerminatedRelationships());
+        processor.setBulletinLevel(config.getBulletinLevel());
+        processor.setBundle(mapBundle(dto.getBundle()));
+        processor.setComments(config.getComments());
+        processor.setConcurrentlySchedulableTaskCount(config.getConcurrentlySchedulableTaskCount());
+        processor.setExecutionNode(config.getExecutionNode());
+        processor.setName(dto.getName());
+        processor.setPenaltyDuration(config.getPenaltyDuration());
+        processor.setPosition(mapPosition(dto.getPosition()));
+        processor.setProperties(config.getProperties());
+        processor.setRunDurationMillis(config.getRunDurationMillis());
+        processor.setSchedulingPeriod(config.getSchedulingPeriod());
+        processor.setSchedulingStrategy(config.getSchedulingStrategy());
+        processor.setStyle(dto.getStyle());
+        processor.setYieldDuration(config.getYieldDuration());
+        return processor;
+    }
+
+    public VersionedRemoteProcessGroup mapRemoteProcessGroup(final RemoteProcessGroupDTO dto) {
+        final VersionedRemoteProcessGroup rpg = new VersionedRemoteProcessGroup();
+        rpg.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        rpg.setGroupIdentifier(getGroupId(dto.getParentGroupId()));
+        rpg.setComments(dto.getComments());
+        rpg.setCommunicationsTimeout(dto.getCommunicationsTimeout());
+        rpg.setLocalNetworkInterface(dto.getLocalNetworkInterface());
+        rpg.setName(dto.getName());
+        rpg.setInputPorts(dto.getContents().getInputPorts().stream()
+            .map(port -> mapRemotePort(port, ComponentType.REMOTE_INPUT_PORT))
+            .collect(Collectors.toSet()));
+        rpg.setOutputPorts(dto.getContents().getOutputPorts().stream()
+            .map(port -> mapRemotePort(port, ComponentType.REMOTE_OUTPUT_PORT))
+            .collect(Collectors.toSet()));
+        rpg.setPosition(mapPosition(dto.getPosition()));
+        rpg.setProxyHost(dto.getProxyHost());
+        rpg.setProxyPort(dto.getProxyPort());
+        rpg.setProxyUser(dto.getProxyUser());
+        rpg.setTargetUri(dto.getTargetUri());
+        rpg.setTargetUris(dto.getTargetUris());
+        rpg.setTransportProtocol(dto.getTransportProtocol());
+        rpg.setYieldDuration(dto.getYieldDuration());
+        return rpg;
+    }
+
+    public VersionedRemoteGroupPort mapRemotePort(final RemoteProcessGroupPortDTO dto, final ComponentType componentType) {
+        final VersionedRemoteGroupPort port = new VersionedRemoteGroupPort();
+        port.setIdentifier(getId(dto.getVersionedComponentId(), dto.getId()));
+        port.setGroupIdentifier(getGroupId(dto.getGroupId()));
+        port.setComments(dto.getComments());
+        port.setConcurrentlySchedulableTaskCount(dto.getConcurrentlySchedulableTaskCount());
+        port.setGroupId(dto.getGroupId());
+        port.setName(dto.getName());
+        port.setUseCompression(dto.getUseCompression());
+        port.setBatchSettings(mapBatchSettings(dto.getBatchSettings()));
+        port.setComponentType(componentType);
+        return port;
+    }
+
+    private BatchSize mapBatchSettings(final BatchSettingsDTO dto) {
+        final BatchSize batchSize = new BatchSize();
+        batchSize.setCount(dto.getCount());
+        batchSize.setDuration(dto.getDuration());
+        batchSize.setSize(dto.getSize());
+        return batchSize;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
new file mode 100644
index 0000000..e3edc30
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
@@ -0,0 +1,397 @@
+/*
+ * 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.registry.flow.mapping;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Funnel;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.ConfiguredComponent;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.registry.flow.BatchSize;
+import org.apache.nifi.registry.flow.Bundle;
+import org.apache.nifi.registry.flow.ComponentType;
+import org.apache.nifi.registry.flow.ConnectableComponent;
+import org.apache.nifi.registry.flow.ConnectableComponentType;
+import org.apache.nifi.registry.flow.ControllerServiceAPI;
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.PortType;
+import org.apache.nifi.registry.flow.Position;
+import org.apache.nifi.registry.flow.RemoteFlowCoordinates;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedConnection;
+import org.apache.nifi.registry.flow.VersionedControllerService;
+import org.apache.nifi.registry.flow.VersionedFunnel;
+import org.apache.nifi.registry.flow.VersionedLabel;
+import org.apache.nifi.registry.flow.VersionedPort;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
+import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
+import org.apache.nifi.remote.RemoteGroupPort;
+
+
+public class NiFiRegistryFlowMapper {
+    // We need to keep a mapping of component id to versionedComponentId as we transform these objects. This way, when
+    // we call #mapConnectable, instead of generating a new UUID for the ConnectableComponent, we can lookup the 'versioned'
+    // identifier based on the comopnent's actual id. We do connections last, so that all components will already have been
+    // created before attempting to create the connection, where the ConnectableDTO is converted.
+    private Map<String, String> versionedComponentIds = new HashMap<>();
+
+    public InstantiatedVersionedProcessGroup mapProcessGroup(final ProcessGroup group, final FlowRegistryClient registryClient) {
+        versionedComponentIds.clear();
+        return mapGroup(group, registryClient, true);
+    }
+
+    private InstantiatedVersionedProcessGroup mapGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean topLevel) {
+        final InstantiatedVersionedProcessGroup versionedGroup = new InstantiatedVersionedProcessGroup(group.getIdentifier(), group.getProcessGroupIdentifier());
+        versionedGroup.setIdentifier(getId(group.getVersionedComponentId(), group.getIdentifier()));
+        versionedGroup.setGroupIdentifier(getGroupId(group.getProcessGroupIdentifier()));
+        versionedGroup.setName(group.getName());
+        versionedGroup.setComments(group.getComments());
+        versionedGroup.setPosition(mapPosition(group.getPosition()));
+
+        // If we are at the 'top level', meaning that the given Process Group is the group that we are creating a VersionedProcessGroup for,
+        // then we don't want to include the RemoteFlowCoordinates; we want to include the group contents. The RemoteFlowCoordinates will be used
+        // only for a child group that is itself version controlled.
+        if (!topLevel) {
+            final VersionControlInformation versionControlInfo = group.getVersionControlInformation();
+            if (versionControlInfo != null) {
+                final RemoteFlowCoordinates coordinates = new RemoteFlowCoordinates();
+                final String registryId = versionControlInfo.getRegistryIdentifier();
+                final FlowRegistry registry = registryClient.getFlowRegistry(registryId);
+                if (registry == null) {
+                    throw new IllegalStateException("Process Group refers to a Flow Registry with ID " + registryId + " but no Flow Registry exists with that ID. Cannot resolve to a URL.");
+                }
+
+                coordinates.setRegistryUrl(registry.getURL());
+                coordinates.setBucketId(versionControlInfo.getBucketIdentifier());
+                coordinates.setFlowId(versionControlInfo.getFlowIdentifier());
+                coordinates.setVersion(versionControlInfo.getVersion());
+
+                // If the Process Group itself is remotely versioned, then we don't want to include its contents
+                // because the contents are remotely managed and not part of the versioning of this Process Group
+                return versionedGroup;
+            }
+        }
+
+        versionedGroup.setControllerServices(group.getControllerServices(false).stream()
+            .map(this::mapControllerService)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setFunnels(group.getFunnels().stream()
+            .map(this::mapFunnel)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setInputPorts(group.getInputPorts().stream()
+            .map(this::mapPort)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setOutputPorts(group.getOutputPorts().stream()
+            .map(this::mapPort)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setLabels(group.getLabels().stream()
+            .map(this::mapLabel)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setProcessors(group.getProcessors().stream()
+            .map(this::mapProcessor)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setRemoteProcessGroups(group.getRemoteProcessGroups().stream()
+            .map(this::mapRemoteProcessGroup)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setProcessGroups(group.getProcessGroups().stream()
+            .map(grp -> mapGroup(grp, registryClient, false))
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setConnections(group.getConnections().stream()
+            .map(this::mapConnection)
+            .collect(Collectors.toCollection(LinkedHashSet::new)));
+
+        versionedGroup.setVariables(group.getVariableRegistry().getVariableMap().entrySet().stream()
+            .collect(Collectors.toMap(entry -> entry.getKey().getName(), Map.Entry::getValue)));
+
+        return versionedGroup;
+    }
+
+    private String getId(final Optional<String> currentVersionedId, final String componentId) {
+        final String versionedId;
+        if (currentVersionedId.isPresent()) {
+            versionedId = currentVersionedId.get();
+        } else {
+            versionedId = UUID.nameUUIDFromBytes(componentId.getBytes(StandardCharsets.UTF_8)).toString();
+        }
+
+        versionedComponentIds.put(componentId, versionedId);
+        return versionedId;
+    }
+
+    private String getGroupId(final String groupId) {
+        return versionedComponentIds.get(groupId);
+    }
+
+    public VersionedConnection mapConnection(final Connection connection) {
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        final VersionedConnection versionedConnection = new InstantiatedVersionedConnection(connection.getIdentifier(), connection.getProcessGroup().getIdentifier());
+        versionedConnection.setIdentifier(getId(connection.getVersionedComponentId(), connection.getIdentifier()));
+        versionedConnection.setGroupIdentifier(getGroupId(connection.getProcessGroup().getIdentifier()));
+        versionedConnection.setName(connection.getName());
+        versionedConnection.setBackPressureDataSizeThreshold(queue.getBackPressureDataSizeThreshold());
+        versionedConnection.setBackPressureObjectThreshold(queue.getBackPressureObjectThreshold());
+        versionedConnection.setFlowFileExpiration(queue.getFlowFileExpiration());
+        versionedConnection.setLabelIndex(connection.getLabelIndex());
+        versionedConnection.setPrioritizers(queue.getPriorities().stream().map(p -> p.getClass().getName()).collect(Collectors.toList()));
+        versionedConnection.setSelectedRelationships(connection.getRelationships().stream().map(Relationship::getName).collect(Collectors.toSet()));
+        versionedConnection.setzIndex(connection.getZIndex());
+
+        versionedConnection.setBends(connection.getBendPoints().stream()
+            .map(this::mapPosition)
+            .collect(Collectors.toList()));
+
+        versionedConnection.setSource(mapConnectable(connection.getSource()));
+        versionedConnection.setDestination(mapConnectable(connection.getDestination()));
+
+        return versionedConnection;
+    }
+
+    public ConnectableComponent mapConnectable(final Connectable connectable) {
+        final ConnectableComponent component = new InstantiatedConnectableComponent(connectable.getIdentifier(), connectable.getProcessGroupIdentifier());
+
+        final Optional<String> versionedId = connectable.getVersionedComponentId();
+        if (versionedId.isPresent()) {
+            component.setId(versionedId.get());
+        } else {
+            final String resolved = versionedComponentIds.get(connectable.getIdentifier());
+            if (resolved == null) {
+                throw new IllegalArgumentException("Unable to map Connectable Component with identifier " + connectable.getIdentifier() + " to any version-controlled component");
+            }
+
+            component.setId(resolved);
+        }
+
+        component.setComments(connectable.getComments());
+        component.setGroupId(connectable.getProcessGroupIdentifier());
+        component.setName(connectable.getName());
+        component.setType(ConnectableComponentType.valueOf(connectable.getConnectableType().name()));
+        return component;
+    }
+
+    public VersionedControllerService mapControllerService(final ControllerServiceNode controllerService) {
+        final VersionedControllerService versionedService = new InstantiatedVersionedControllerService(controllerService.getIdentifier(), controllerService.getProcessGroupIdentifier());
+        versionedService.setIdentifier(getId(controllerService.getVersionedComponentId(), controllerService.getIdentifier()));
+        versionedService.setGroupIdentifier(getGroupId(controllerService.getProcessGroupIdentifier()));
+        versionedService.setName(controllerService.getName());
+        versionedService.setAnnotationData(controllerService.getAnnotationData());
+        versionedService.setBundle(mapBundle(controllerService.getBundleCoordinate()));
+        versionedService.setComments(controllerService.getComments());
+
+        versionedService.setControllerServiceApis(mapControllerServiceApis(controllerService));
+        versionedService.setProperties(mapProperties(controllerService));
+        versionedService.setType(controllerService.getCanonicalClassName());
+
+        return versionedService;
+    }
+
+    private Map<String, String> mapProperties(final ConfiguredComponent component) {
+        final Map<String, String> mapped = new HashMap<>();
+        component.getProperties().keySet().stream()
+            .forEach(property -> {
+                String value = component.getProperty(property);
+                if (value == null) {
+                    value = property.getDefaultValue();
+                }
+                mapped.put(property.getName(), value);
+            });
+        return mapped;
+    }
+
+    private Bundle mapBundle(final BundleCoordinate coordinate) {
+        final Bundle versionedBundle = new Bundle();
+        versionedBundle.setGroup(coordinate.getGroup());
+        versionedBundle.setArtifact(coordinate.getId());
+        versionedBundle.setVersion(coordinate.getVersion());
+        return versionedBundle;
+    }
+
+    private List<ControllerServiceAPI> mapControllerServiceApis(final ControllerServiceNode service) {
+        final Class<?> serviceClass = service.getControllerServiceImplementation().getClass();
+
+        final Set<Class<?>> serviceApiClasses = new HashSet<>();
+        // get all of it's interfaces to determine the controller service api's it implements
+        final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(serviceClass);
+        for (final Class<?> i : interfaces) {
+            // add all controller services that's not ControllerService itself
+            if (ControllerService.class.isAssignableFrom(i) && !ControllerService.class.equals(i)) {
+                serviceApiClasses.add(i);
+            }
+        }
+
+
+        final List<ControllerServiceAPI> serviceApis = new ArrayList<>();
+        for (final Class<?> serviceApiClass : serviceApiClasses) {
+            final BundleCoordinate bundleCoordinate = ExtensionManager.getBundle(serviceApiClass.getClassLoader()).getBundleDetails().getCoordinate();
+
+            final ControllerServiceAPI serviceApi = new ControllerServiceAPI();
+            serviceApi.setType(serviceApiClass.getName());
+            serviceApi.setBundle(mapBundle(bundleCoordinate));
+            serviceApis.add(serviceApi);
+        }
+        return serviceApis;
+    }
+
+
+    public VersionedFunnel mapFunnel(final Funnel funnel) {
+        final VersionedFunnel versionedFunnel = new InstantiatedVersionedFunnel(funnel.getIdentifier(), funnel.getProcessGroupIdentifier());
+        versionedFunnel.setIdentifier(getId(funnel.getVersionedComponentId(), funnel.getIdentifier()));
+        versionedFunnel.setGroupIdentifier(getGroupId(funnel.getProcessGroupIdentifier()));
+        versionedFunnel.setPosition(mapPosition(funnel.getPosition()));
+
+        return versionedFunnel;
+    }
+
+    public VersionedLabel mapLabel(final Label label) {
+        final VersionedLabel versionedLabel = new InstantiatedVersionedLabel(label.getIdentifier(), label.getProcessGroupIdentifier());
+        versionedLabel.setIdentifier(getId(label.getVersionedComponentId(), label.getIdentifier()));
+        versionedLabel.setGroupIdentifier(getGroupId(label.getProcessGroupIdentifier()));
+        versionedLabel.setHeight(label.getSize().getHeight());
+        versionedLabel.setWidth(label.getSize().getWidth());
+        versionedLabel.setLabel(label.getValue());
+        versionedLabel.setPosition(mapPosition(label.getPosition()));
+        versionedLabel.setStyle(label.getStyle());
+
+        return versionedLabel;
+    }
+
+    public VersionedPort mapPort(final Port port) {
+        final VersionedPort versionedPort = new InstantiatedVersionedPort(port.getIdentifier(), port.getProcessGroupIdentifier());
+        versionedPort.setIdentifier(getId(port.getVersionedComponentId(), port.getIdentifier()));
+        versionedPort.setGroupIdentifier(getGroupId(port.getProcessGroupIdentifier()));
+        versionedPort.setComments(port.getComments());
+        versionedPort.setConcurrentlySchedulableTaskCount(port.getMaxConcurrentTasks());
+        versionedPort.setName(port.getName());
+        versionedPort.setPosition(mapPosition(port.getPosition()));
+        versionedPort.setType(PortType.valueOf(port.getComponentType()));
+        return versionedPort;
+    }
+
+    public Position mapPosition(final org.apache.nifi.connectable.Position pos) {
+        final Position position = new Position();
+        position.setX(pos.getX());
+        position.setY(pos.getY());
+        return position;
+    }
+
+    public VersionedProcessor mapProcessor(final ProcessorNode procNode) {
+        final VersionedProcessor processor = new InstantiatedVersionedProcessor(procNode.getIdentifier(), procNode.getProcessGroupIdentifier());
+        processor.setIdentifier(getId(procNode.getVersionedComponentId(), procNode.getIdentifier()));
+        processor.setGroupIdentifier(getGroupId(procNode.getProcessGroupIdentifier()));
+        processor.setType(procNode.getCanonicalClassName());
+        processor.setAnnotationData(procNode.getAnnotationData());
+        processor.setAutoTerminatedRelationships(procNode.getAutoTerminatedRelationships().stream().map(Relationship::getName).collect(Collectors.toSet()));
+        processor.setBulletinLevel(procNode.getBulletinLevel().name());
+        processor.setBundle(mapBundle(procNode.getBundleCoordinate()));
+        processor.setComments(procNode.getComments());
+        processor.setConcurrentlySchedulableTaskCount(procNode.getMaxConcurrentTasks());
+        processor.setExecutionNode(procNode.getExecutionNode().name());
+        processor.setName(procNode.getName());
+        processor.setPenaltyDuration(procNode.getPenalizationPeriod());
+        processor.setPosition(mapPosition(procNode.getPosition()));
+        processor.setProperties(mapProperties(procNode));
+        processor.setRunDurationMillis(procNode.getRunDuration(TimeUnit.MILLISECONDS));
+        processor.setSchedulingPeriod(procNode.getSchedulingPeriod());
+        processor.setSchedulingStrategy(procNode.getSchedulingStrategy().name());
+        processor.setStyle(procNode.getStyle());
+        processor.setYieldDuration(procNode.getYieldPeriod());
+
+        return processor;
+    }
+
+    public VersionedRemoteProcessGroup mapRemoteProcessGroup(final RemoteProcessGroup remoteGroup) {
+        final VersionedRemoteProcessGroup rpg = new InstantiatedVersionedRemoteProcessGroup(remoteGroup.getIdentifier(), remoteGroup.getProcessGroupIdentifier());
+        rpg.setIdentifier(getId(remoteGroup.getVersionedComponentId(), remoteGroup.getIdentifier()));
+        rpg.setGroupIdentifier(getGroupId(remoteGroup.getProcessGroupIdentifier()));
+        rpg.setComments(remoteGroup.getComments());
+        rpg.setCommunicationsTimeout(remoteGroup.getCommunicationsTimeout());
+        rpg.setLocalNetworkInterface(remoteGroup.getNetworkInterface());
+        rpg.setName(remoteGroup.getName());
+        rpg.setInputPorts(remoteGroup.getInputPorts().stream()
+            .map(port -> mapRemotePort(port, ComponentType.REMOTE_INPUT_PORT))
+            .collect(Collectors.toSet()));
+        rpg.setOutputPorts(remoteGroup.getOutputPorts().stream()
+            .map(port -> mapRemotePort(port, ComponentType.REMOTE_OUTPUT_PORT))
+            .collect(Collectors.toSet()));
+        rpg.setPosition(mapPosition(remoteGroup.getPosition()));
+        rpg.setProxyHost(remoteGroup.getProxyHost());
+        rpg.setProxyPort(remoteGroup.getProxyPort());
+        rpg.setProxyUser(remoteGroup.getProxyUser());
+        rpg.setTargetUri(remoteGroup.getTargetUri());
+        rpg.setTargetUris(remoteGroup.getTargetUris());
+        rpg.setTransportProtocol(remoteGroup.getTransportProtocol().name());
+        rpg.setYieldDuration(remoteGroup.getYieldDuration());
+        return rpg;
+    }
+
+    public VersionedRemoteGroupPort mapRemotePort(final RemoteGroupPort remotePort, final ComponentType componentType) {
+        final VersionedRemoteGroupPort port = new InstantiatedVersionedRemoteGroupPort(remotePort.getIdentifier(), remotePort.getRemoteProcessGroup().getIdentifier());
+        port.setIdentifier(getId(remotePort.getVersionedComponentId(), remotePort.getIdentifier()));
+        port.setGroupIdentifier(getGroupId(remotePort.getRemoteProcessGroup().getIdentifier()));
+        port.setComments(remotePort.getComments());
+        port.setConcurrentlySchedulableTaskCount(remotePort.getMaxConcurrentTasks());
+        port.setGroupId(remotePort.getProcessGroupIdentifier());
+        port.setName(remotePort.getName());
+        port.setUseCompression(remotePort.isUseCompression());
+        port.setBatchSettings(mapBatchSettings(remotePort));
+        port.setComponentType(componentType);
+        return port;
+    }
+
+    private BatchSize mapBatchSettings(final RemoteGroupPort remotePort) {
+        final BatchSize batchSize = new BatchSize();
+        batchSize.setCount(remotePort.getBatchCount());
+        batchSize.setDuration(remotePort.getBatchDuration());
+        batchSize.setSize(remotePort.getBatchSize());
+        return batchSize;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index 53d5c9f..6b55735 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -34,6 +34,7 @@ import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ScheduledExecutorService;
@@ -109,6 +110,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
     private final AtomicReference<String> comments = new AtomicReference<>();
     private final AtomicReference<ProcessGroup> processGroup;
     private final AtomicBoolean transmitting = new AtomicBoolean(false);
+    private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
     private final SSLContext sslContext;
 
     private volatile String communicationsTimeout = "30 sec";
@@ -1419,4 +1421,26 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
         return new File(stateDir, getIdentifier() + ".peers");
     }
 
+    @Override
+    public Optional<String> getVersionedComponentId() {
+        return Optional.ofNullable(versionedComponentId.get());
+    }
+
+    @Override
+    public void setVersionedComponentId(final String versionedComponentId) {
+        boolean updated = false;
+        while (!updated) {
+            final String currentId = this.versionedComponentId.get();
+
+            if (currentId == null) {
+                updated = this.versionedComponentId.compareAndSet(null, versionedComponentId);
+            } else if (currentId.equals(versionedComponentId)) {
+                return;
+            } else if (versionedComponentId == null) {
+                updated = this.versionedComponentId.compareAndSet(currentId, null);
+            } else {
+                throw new IllegalStateException(this + " is already under version control");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroupPortDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroupPortDescriptor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroupPortDescriptor.java
index 7dbcec1..7d657ce 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroupPortDescriptor.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroupPortDescriptor.java
@@ -22,6 +22,7 @@ public class StandardRemoteProcessGroupPortDescriptor implements RemoteProcessGr
 
     private String id;
     private String targetId;
+    private String versionedComponentId;
     private String groupId;
     private String name;
     private String comments;
@@ -185,4 +186,13 @@ public class StandardRemoteProcessGroupPortDescriptor implements RemoteProcessGr
         }
         return name.equals(other.getName());
     }
+
+    @Override
+    public String getVersionedComponentId() {
+        return versionedComponentId;
+    }
+
+    public void setVersionedComponentId(String versionedId) {
+        this.versionedComponentId = versionedId;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
index 7ed9187..c7a7e7d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/FlowControllerFactoryBean.java
@@ -26,6 +26,7 @@ import org.apache.nifi.controller.leader.election.LeaderElectionManager;
 import org.apache.nifi.controller.repository.FlowFileEventRepository;
 import org.apache.nifi.encrypt.StringEncryptor;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.util.NiFiProperties;
 import org.springframework.beans.BeansException;
@@ -49,6 +50,7 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
     private ClusterCoordinator clusterCoordinator;
     private VariableRegistry variableRegistry;
     private LeaderElectionManager leaderElectionManager;
+    private FlowRegistryClient flowRegistryClient;
 
     @Override
     public Object getObject() throws Exception {
@@ -69,7 +71,8 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
                     clusterCoordinator,
                     heartbeatMonitor,
                     leaderElectionManager,
-                    variableRegistry);
+                    variableRegistry,
+                    flowRegistryClient);
             } else {
                 flowController = FlowController.createStandaloneInstance(
                     flowFileEventRepository,
@@ -77,7 +80,9 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
                     authorizer,
                     auditService,
                     encryptor,
-                    bulletinRepository, variableRegistry);
+                    bulletinRepository,
+                    variableRegistry,
+                    flowRegistryClient);
             }
 
         }
@@ -133,4 +138,8 @@ public class FlowControllerFactoryBean implements FactoryBean, ApplicationContex
     public void setLeaderElectionManager(final LeaderElectionManager leaderElectionManager) {
         this.leaderElectionManager = leaderElectionManager;
     }
+
+    public void setFlowRegistryClient(final FlowRegistryClient flowRegistryClient) {
+        this.flowRegistryClient = flowRegistryClient;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
index 19e1c55..8186c8b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
@@ -45,6 +45,7 @@
             of unexpected process termination or execution failure.  The "id" should rarely change and if it
             must then the database directory should be deleted to be safe.-->
             <xs:element name="id" type="NonEmptyStringType"/>
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
 
             <!-- The "name" is a nicely displayable description of the processor's duty-->
             <xs:element name="name" type="NonEmptyStringType"/>
@@ -130,10 +131,12 @@
     <xs:complexType name="ProcessGroupType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="position" type="PositionType" />
             <xs:element name="comment" type="xs:string" />
-    		
+    		<xs:element name="versionControlInformation" type="VersionControlInformation" minOccurs="0" maxOccurs="1" />
+            
             <!-- Each "processor" defines the actual dataflow work horses that make dataflow happen-->
             <xs:element name="processor" type="ProcessorType" minOccurs="0" maxOccurs="unbounded"/>
 
@@ -156,11 +159,21 @@
         <xs:attribute name="value" />
     </xs:complexType>
     
+    <xs:complexType name="VersionControlInformation">
+        <xs:sequence>
+            <xs:element name="registryId" type="NonEmptyStringType" />
+            <xs:element name="bucketId" type="NonEmptyStringType" />
+            <xs:element name="flowId" type="NonEmptyStringType" />
+            <xs:element name="version" type="NonEmptyStringType" />
+        </xs:sequence>
+    </xs:complexType>
+    
     <!-- Same as ProcessGroupType except that instead of input ports & output ports being of type PortType,
     they are of type RootGroupPortType -->
     <xs:complexType name="RootProcessGroupType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="position" type="PositionType" />
             <xs:element name="comment" type="xs:string" />
@@ -184,6 +197,7 @@
     <xs:complexType name="FunnelType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="position" type="PositionType" />
         </xs:sequence>
     </xs:complexType>
@@ -191,6 +205,7 @@
     <xs:complexType name="LabelType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="position" type="PositionType" />
             <xs:element name="size" type="SizeType" />
             <xs:element name="styles" type="Styles" />
@@ -201,6 +216,7 @@
     <xs:complexType name="RemoteProcessGroupType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="name" type="xs:string" />
             <xs:element name="position" type="PositionType" />
             <xs:element name="comment" type="xs:string" />
@@ -227,6 +243,7 @@
     <xs:complexType name="ConnectionType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="name" type="xs:string" />
             <xs:element name="bendPoints" type="BendPointsType" minOccurs="0" maxOccurs="1" />
             <xs:element name="labelIndex" type="xs:int" minOccurs="0" maxOccurs="1" />
@@ -262,6 +279,7 @@
     <xs:complexType name="PortType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="position" type="PositionType" />
             <xs:element name="comments" type="xs:string" />
@@ -384,6 +402,7 @@
     <xs:complexType name="ControllerServiceType">
         <xs:sequence>
             <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="versionedComponentId" type="NonEmptyStringType" minOccurs="0" maxOccurs="1" />
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="comment" type="xs:string" />
             <xs:element name="class" type="NonEmptyStringType" />

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
index b3c426c..6a3ec8b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
@@ -35,6 +35,11 @@
         <constructor-arg ref="nifiProperties" />
     </bean>
 
+    <!-- flow registry -->
+    <bean id="flowRegistryClient" class="org.apache.nifi.registry.flow.FileBasedFlowRegistryClient">
+        <constructor-arg index="0" type="java.io.File" value="../flowRegistry" />
+    </bean>
+
     <!-- flow controller -->
     <bean id="flowController" class="org.apache.nifi.spring.FlowControllerFactoryBean">
         <property name="properties" ref="nifiProperties"/>
@@ -45,6 +50,7 @@
         <property name="clusterCoordinator" ref="clusterCoordinator" />
         <property name="variableRegistry" ref="variableRegistry"/>
         <property name="leaderElectionManager" ref="leaderElectionManager" />
+        <property name="flowRegistryClient" ref="flowRegistryClient" />
     </bean>
 
     <!-- flow service -->

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
index 6973d12..392e92b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/StandardFlowServiceTest.java
@@ -28,6 +28,7 @@ import org.apache.nifi.controller.serialization.StandardFlowSerializer;
 import org.apache.nifi.encrypt.StringEncryptor;
 import org.apache.nifi.events.VolatileBulletinRepository;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.variable.FileBasedVariableRegistry;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.api.dto.ConnectableDTO;
@@ -86,7 +87,7 @@ public class StandardFlowServiceTest {
         mockAuditService = mock(AuditService.class);
         revisionManager = mock(RevisionManager.class);
         flowController = FlowController.createStandaloneInstance(mockFlowFileEventRepository, properties, authorizer, mockAuditService, mockEncryptor,
-                                        new VolatileBulletinRepository(), variableRegistry);
+                                        new VolatileBulletinRepository(), variableRegistry, mock(FlowRegistryClient.class));
         flowService = StandardFlowService.createStandaloneInstance(flowController, properties, mockEncryptor, revisionManager, authorizer);
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
index 596c00f..d9728b2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
@@ -49,6 +49,7 @@ import org.apache.nifi.nar.SystemBundle;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.provenance.MockProvenanceRepository;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.variable.FileBasedVariableRegistry;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.scheduling.SchedulingStrategy;
@@ -162,7 +163,8 @@ public class TestFlowController {
         variableRegistry = new FileBasedVariableRegistry(nifiProperties.getVariableRegistryPropertiesPaths());
 
         bulletinRepo = Mockito.mock(BulletinRepository.class);
-        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer, auditService, encryptor, bulletinRepo, variableRegistry);
+        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer,
+            auditService, encryptor, bulletinRepo, variableRegistry, Mockito.mock(FlowRegistryClient.class));
     }
 
     @After
@@ -323,7 +325,8 @@ public class TestFlowController {
         assertNotEquals(authFingerprint, authorizer.getFingerprint());
 
         controller.shutdown(true);
-        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer, auditService, encryptor, bulletinRepo, variableRegistry);
+        controller = FlowController.createStandaloneInstance(flowFileEventRepo, nifiProperties, authorizer,
+            auditService, encryptor, bulletinRepo, variableRegistry, Mockito.mock(FlowRegistryClient.class));
         controller.synchronize(standardFlowSynchronizer, proposedDataFlow);
         assertEquals(authFingerprint, authorizer.getFingerprint());
     }


[11/50] nifi git commit: NIFI-4436: - Initial checkpoint: able ot start version control and detect changes, in standalone mode, still 'crude' implementation - Checkpoint: Can place flow under version control and can determine if modified - Checkpoint: Ch

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6a58d780/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
new file mode 100644
index 0000000..d3a11dc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -0,0 +1,1409 @@
+/*
+ * 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.web.api;
+
+import com.sun.jersey.api.client.ClientResponse.Status;
+import com.sun.jersey.api.core.ResourceContext;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.registry.flow.Bundle;
+import org.apache.nifi.registry.flow.ComponentType;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.util.BundleUtils;
+import org.apache.nifi.web.NiFiServiceFacade;
+import org.apache.nifi.web.ResourceNotFoundException;
+import org.apache.nifi.web.Revision;
+import org.apache.nifi.web.api.concurrent.AsyncRequestManager;
+import org.apache.nifi.web.api.concurrent.AsynchronousWebRequest;
+import org.apache.nifi.web.api.concurrent.RequestManager;
+import org.apache.nifi.web.api.concurrent.StandardAsynchronousWebRequest;
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.BundleDTO;
+import org.apache.nifi.web.api.dto.DtoFactory;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
+import org.apache.nifi.web.api.dto.VersionedFlowDTO;
+import org.apache.nifi.web.api.dto.VersionedFlowUpdateRequestDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
+import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowSnapshotEntity;
+import org.apache.nifi.web.api.entity.VersionedFlowUpdateRequestEntity;
+import org.apache.nifi.web.api.request.ClientIdParameter;
+import org.apache.nifi.web.api.request.LongParameter;
+import org.apache.nifi.web.util.AffectedComponentUtils;
+import org.apache.nifi.web.util.CancellableTimedPause;
+import org.apache.nifi.web.util.ComponentLifecycle;
+import org.apache.nifi.web.util.LifecycleManagementException;
+import org.apache.nifi.web.util.Pause;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Path("/versions")
+@Api(value = "/versions", description = "Endpoint for managing version control for a flow")
+public class VersionsResource extends ApplicationResource {
+    private static final Logger logger = LoggerFactory.getLogger(VersionsResource.class);
+
+    @Context
+    private ResourceContext resourceContext;
+    private NiFiServiceFacade serviceFacade;
+    private Authorizer authorizer;
+    private ComponentLifecycle clusterComponentLifecycle;
+    private ComponentLifecycle localComponentLifecycle;
+    private DtoFactory dtoFactory;
+
+    private RequestManager<VersionControlInformationEntity> requestManager = new AsyncRequestManager<>(100, TimeUnit.MINUTES.toMillis(1L), "Version Control Update Thread");
+
+    // We need to ensure that only a single Version Control Request can occur throughout the flow.
+    // Otherwise, User 1 could log into Node 1 and choose to Version Control Group A.
+    // At the same time, User 2 could log into Node 2 and choose to Version Control Group B, which is a child of Group A.
+    // As a result, only one of the requests would succeed (the other would be rejected due to the Revision being wrong).
+    // However, the request that was rejected may well have already caused the flow to be added to the Flow Registry,
+    // so we would have created a flow in the Flow Registry that will never be referenced and will essentially be "orphaned."
+    private ActiveRequest activeRequest = null;
+    private final Object activeRequestMonitor = new Object();
+
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("process-groups/{id}")
+    @ApiOperation(value = "Gets the Version Control information for a process group", response = VersionControlInformationEntity.class, authorizations = {
+        @Authorization(value = "Read - /process-groups/{uuid}")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getVersionInformation(@ApiParam(value = "The process group id.", required = true) @PathParam("id") final String groupId) {
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.GET);
+        }
+
+        // authorize access
+        serviceFacade.authorizeAccess(lookup -> {
+            final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
+            processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+        });
+
+        // get the version control information for this process group
+        final VersionControlInformationEntity entity = serviceFacade.getVersionControlInformation(groupId);
+        if (entity == null) {
+            throw new ResourceNotFoundException("Process Group with ID " + groupId + " is not currently under Version Control");
+        }
+
+        return generateOkResponse(entity).build();
+    }
+
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("start-requests")
+    @ApiOperation(value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed", response = VersionControlInformationEntity.class)
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response createVersionControlRequest() throws InterruptedException {
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.POST);
+        }
+
+        return withWriteLock(
+            serviceFacade,
+            /* entity */ null,
+            lookup -> {
+            },
+            /* verifier */ null,
+            requestEntity -> {
+                final String requestId = generateUuid();
+
+                // We need to ensure that only a single Version Control Request can occur throughout the flow.
+                // Otherwise, User 1 could log into Node 1 and choose to Version Control Group A.
+                // At the same time, User 2 could log into Node 2 and choose to Version Control Group B, which is a child of Group A.
+                // As a result, may could end up in a situation where we are creating flows in the registry that are never referenced.
+                synchronized (activeRequestMonitor) {
+                    if (activeRequest == null || activeRequest.isExpired()) {
+                        activeRequest = new ActiveRequest(requestId);
+                    } else {
+                        throw new IllegalStateException("A request is already underway to place a Process Group in this NiFi instance under Version Control. "
+                            + "Only a single such request is allowed to occurred at a time. Please try the request again momentarily.");
+                    }
+                }
+
+                return generateOkResponse(requestId).build();
+            });
+    }
+
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("start-requests/{id}")
+    @ApiOperation(value = "Updates the request with the given ID", response = VersionControlInformationEntity.class, authorizations = {
+        @Authorization(value = "Write - /process-groups/{uuid}")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response updateVersionControlRequest(@ApiParam("The request ID.") @PathParam("id") final String requestId,
+        @ApiParam(value = "The controller service configuration details.", required = true) final VersionControlComponentMappingEntity requestEntity) {
+
+        // Verify request
+        final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+        if (revisionDto == null) {
+            throw new IllegalArgumentException("Process Group Revision must be specified");
+        }
+
+        final VersionControlInformationDTO versionControlInfo = requestEntity.getVersionControlInformation();
+        if (versionControlInfo == null) {
+            throw new IllegalArgumentException("Version Control Information must be supplied");
+        }
+        if (versionControlInfo.getGroupId() == null) {
+            throw new IllegalArgumentException("Version Control Information must supply Process Group ID");
+        }
+        if (versionControlInfo.getBucketId() == null) {
+            throw new IllegalArgumentException("Version Control Information must supply Bucket ID");
+        }
+        if (versionControlInfo.getFlowId() == null) {
+            throw new IllegalArgumentException("Version Control Information must supply Flow ID");
+        }
+        if (versionControlInfo.getRegistryId() == null) {
+            throw new IllegalArgumentException("Version Control Information must supply Registry ID");
+        }
+        if (versionControlInfo.getVersion() == null) {
+            throw new IllegalArgumentException("Version Control Information must supply Version");
+        }
+
+        final Map<String, String> mapping = requestEntity.getVersionControlComponentMapping();
+        if (mapping == null) {
+            throw new IllegalArgumentException("Version Control Component Mapping must be supplied");
+        }
+
+        // Replicate if necessary
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.PUT, requestEntity);
+        }
+
+        // Perform the update
+        synchronized (activeRequestMonitor) {
+            if (activeRequest == null) {
+                throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+            }
+
+            if (!requestId.equals(activeRequest.getRequestId())) {
+                throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+            }
+
+            if (activeRequest.isExpired()) {
+                throw new IllegalStateException("Version Control Request with ID " + requestId + " has already expired");
+            }
+
+            final String groupId = requestEntity.getVersionControlInformation().getGroupId();
+
+            final Revision groupRevision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
+            return withWriteLock(
+                serviceFacade,
+                requestEntity,
+                groupRevision,
+                lookup -> {
+                    final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
+                    processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                },
+                null,
+                (rev, mappingEntity) -> {
+                    final VersionControlInformationEntity responseEntity = serviceFacade.setVersionControlInformation(rev, groupId,
+                        mappingEntity.getVersionControlInformation(), mappingEntity.getVersionControlComponentMapping());
+                    return generateOkResponse(responseEntity).build();
+                });
+        }
+    }
+
+
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("start-requests/{id}")
+    @ApiOperation(value = "Deletes the request with the given ID")
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response deleteVersionControlRequest(@ApiParam("The request ID.") @PathParam("id") final String requestId) {
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.DELETE);
+        }
+
+        return withWriteLock(
+            serviceFacade,
+            null,
+            lookup -> {
+            },
+            null,
+            requestEntity -> {
+                synchronized (activeRequestMonitor) {
+                    if (activeRequest == null) {
+                        throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+                    }
+
+                    if (!requestId.equals(activeRequest.getRequestId())) {
+                        throw new IllegalStateException("No Version Control Request with ID " + requestId + " is currently active");
+                    }
+
+                    activeRequest = null;
+                }
+
+                return generateOkResponse().build();
+            });
+
+    }
+
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("process-groups/{id}")
+    @ApiOperation(value = "Begins version controlling the Process Group with the given ID", response = VersionControlInformationEntity.class, authorizations = {
+        @Authorization(value = "Read - /process-groups/{uuid}")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response startVersionControl(
+        @ApiParam("The process group id.") @PathParam("id") final String groupId,
+        @ApiParam(value = "The versioned flow details.", required = true) final VersionedFlowEntity requestEntity) throws IOException {
+
+        // Verify the request
+        final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+        if (revisionDto == null) {
+            throw new IllegalArgumentException("Process Group Revision must be specified");
+        }
+
+        final VersionedFlowDTO versionedFlowDto = requestEntity.getVersionedFlow();
+        if (versionedFlowDto == null) {
+            throw new IllegalArgumentException("Version Control Information must be supplied.");
+        }
+        if (versionedFlowDto.getBucketId() == null) {
+            throw new IllegalArgumentException("The Bucket ID must be supplied.");
+        }
+        if (versionedFlowDto.getFlowName() == null && versionedFlowDto.getFlowId() == null) {
+            throw new IllegalArgumentException("The Flow Name or Flow ID must be supplied.");
+        }
+        if (versionedFlowDto.getRegistryId() == null) {
+            throw new IllegalArgumentException("The Registry ID must be supplied.");
+        }
+
+        if (isReplicateRequest()) {
+            // We first have to obtain a "lock" on all nodes in the cluster so that multiple Version Control requests
+            // are not being made simultaneously. We do this by making a POST to /nifi-api/versions/start-requests.
+            // The Response gives us back the Request ID.
+            final URI requestUri;
+            try {
+                final URI originalUri = getAbsolutePath();
+                final URI createRequestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+                    originalUri.getPort(), "/nifi-api/versions/start-requests", null, originalUri.getFragment());
+
+                final NodeResponse clusterResponse;
+                try {
+                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                        clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                    } else {
+                        clusterResponse = getRequestReplicator().forwardToCoordinator(
+                            getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                    }
+                } catch (final InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                    throw new RuntimeException("Interrupted while updating Version Control Information for Process Group with ID " + groupId + ".", ie);
+                }
+
+                if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+                    final String errorResponse = getResponseEntity(clusterResponse, String.class);
+                    throw new IllegalStateException(
+                        "Failed to create a Version Control Request across all nodes in the cluster. Received response code " + clusterResponse.getStatus() + " with content: " + errorResponse);
+                }
+
+                final String requestId = getResponseEntity(clusterResponse, String.class);
+
+                requestUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+                    originalUri.getPort(), "/nifi-api/versions/start-requests/" + requestId, null, originalUri.getFragment());
+            } catch (final URISyntaxException e) {
+                throw new RuntimeException(e);
+            }
+
+
+            // Now that we have the Request, we know that no other thread is updating the Flow Registry. So we can now
+            // create the Flow in the Flow Registry and push the Process Group as the first version of the Flow. Once we've
+            // succeeded with that, we need to update all nodes' Process Group to contain the new Version Control Information.
+            // Finally, we can delete the Request.
+            try {
+                final VersionControlComponentMappingEntity mappingEntity = serviceFacade.registerFlowWithFlowRegistry(groupId, requestEntity);
+
+                final Map<String, String> headers = new HashMap<>();
+                headers.put("content-type", MediaType.APPLICATION_JSON);
+
+                final NodeResponse clusterResponse;
+                try {
+                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                        clusterResponse = getRequestReplicator().replicate(HttpMethod.PUT, requestUri, mappingEntity, headers).awaitMergedResponse();
+                    } else {
+                        clusterResponse = getRequestReplicator().forwardToCoordinator(
+                            getClusterCoordinatorNode(), HttpMethod.PUT, requestUri, mappingEntity, headers).awaitMergedResponse();
+                    }
+                } catch (final InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                    throw new RuntimeException("Interrupted while updating Version Control Information for Process Group with ID " + groupId + ".", ie);
+                }
+
+                if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+                    final String message = "Failed to update Version Control Information for Process Group with ID " + groupId + ".";
+                    final Throwable cause = clusterResponse.getThrowable();
+                    if (cause == null) {
+                        throw new IllegalStateException(message);
+                    } else {
+                        throw new IllegalStateException(message, cause);
+                    }
+                }
+
+                final VersionControlInformationEntity responseEntity = serviceFacade.getVersionControlInformation(groupId);
+                return generateOkResponse(responseEntity).build();
+            } finally {
+                final NodeResponse clusterResponse;
+                try {
+                    if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                        clusterResponse = getRequestReplicator().replicate(HttpMethod.DELETE, requestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                    } else {
+                        clusterResponse = getRequestReplicator().forwardToCoordinator(
+                            getClusterCoordinatorNode(), HttpMethod.DELETE, requestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                    }
+                } catch (final InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                    throw new RuntimeException("After starting Version Control on Process Group with ID " + groupId + ", interrupted while waiting for deletion of Version Control Request. "
+                        + "Users may be unable to Version Control other Process Groups until the request lock times out.", ie);
+                }
+
+                if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
+                    logger.error("After starting Version Control on Process Group with ID " + groupId + ", failed to delete Version Control Request. "
+                        + "Users may be unable to Version Control other Process Groups until the request lock times out. Response status code was " + clusterResponse.getStatus());
+                }
+            }
+        }
+
+        // Perform local task. If running in a cluster environment, we will never get to this point. This is because
+        // in the above block, we check if (isReplicate()) and if true, we implement the 'cluster logic', but this
+        // does not involve replicating the actual request, because we only want a single node to handle the logic of
+        // creating the flow in the Registry.
+        final Revision groupRevision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
+        return withWriteLock(
+            serviceFacade,
+            requestEntity,
+            groupRevision,
+            lookup -> {
+                final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
+                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+            },
+            () -> {
+                final VersionControlInformationEntity entity = serviceFacade.getVersionControlInformation(groupId);
+                if (entity != null) {
+                    final String flowId = requestEntity.getVersionedFlow().getFlowId();
+                    if (flowId != null && flowId.equals(entity.getVersionControlInformation().getFlowId())) {
+                        // Flow ID is the same. We want to publish the Process Group as the next version of the Flow.
+                        // In order to do this, we have to ensure that the Process Group is 'current'.
+                        final Boolean current = entity.getVersionControlInformation().getCurrent();
+                        if (current == null) {
+                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
+                                + " because it is not yet known whether or not this Process Group is the most recent version of the flow. "
+                                + "Please try the request again after the Process Group has been synchronized with the Flow Registry.");
+                        }
+
+                        if (current == Boolean.FALSE) {
+                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
+                                + " because the Process Group in the flow is not synchronized with the most recent version of the Flow in the Flow Registry. "
+                                + "In order to publish a new version of the Flow, the Process Group must first be in synch with the latest version in the Flow Registry.");
+                        }
+
+                        // Flow ID matches. We want to publish the Process Group as the next version of the Flow, so we must
+                        // ensure that all other parameters match as well.
+                        if (!requestEntity.getVersionedFlow().getBucketId().equals(entity.getVersionControlInformation().getBucketId())) {
+                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
+                                + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
+                        }
+
+                        if (!requestEntity.getVersionedFlow().getRegistryId().equals(entity.getVersionControlInformation().getRegistryId())) {
+                            throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
+                                + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
+                        }
+
+                    } else if (flowId != null) {
+                        // Flow ID is specified but different. This is not allowed, because Flow ID's are automatically generated,
+                        // and if the client is specifying an ID then it is either trying to assign the ID of the Flow or it is
+                        // attempting to save a new version of a different flow. Saving a new version of a different Flow is
+                        // not allowed because the Process Group must be in synch with the latest version of the flow before that
+                        // can be done.
+                        throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + groupId
+                            + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
+                    }
+                }
+            },
+            (rev, flowEntity) -> {
+                // Register the current flow with the Flow Registry.
+                final VersionControlComponentMappingEntity mappingEntity = serviceFacade.registerFlowWithFlowRegistry(groupId, requestEntity);
+
+                // Update the Process Group's Version Control Information
+                final VersionControlInformationEntity responseEntity = serviceFacade.setVersionControlInformation(rev, groupId,
+                    mappingEntity.getVersionControlInformation(), mappingEntity.getVersionControlComponentMapping());
+                return generateOkResponse(responseEntity).build();
+            });
+    }
+
+
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("process-groups/{id}")
+    @ApiOperation(value = "Stops version controlling the Process Group with the given ID", response = VersionControlInformationEntity.class, authorizations = {
+        @Authorization(value = "Read - /process-groups/{uuid}"),
+        @Authorization(value = "Write - /process-groups/{uuid}"),
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response stopVersionControl(
+        @ApiParam(value = "The version is used to verify the client is working with the latest version of the flow.", required = false) @QueryParam(VERSION) final LongParameter version,
+
+        @ApiParam(value = "If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.", required = false) @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) final ClientIdParameter clientId,
+
+        @ApiParam("The process group id.") @PathParam("id") final String groupId) throws IOException {
+
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.DELETE);
+        }
+
+        final Revision requestRevision = new Revision(version == null ? null : version.getLong(), clientId.getClientId(), groupId);
+        return withWriteLock(
+            serviceFacade,
+            null,
+            requestRevision,
+            lookup -> {
+                final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
+                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+            },
+            () -> {
+                final VersionControlInformationEntity currentVersionControlInfo = serviceFacade.getVersionControlInformation(groupId);
+                if (currentVersionControlInfo == null) {
+                    throw new IllegalStateException("Process Group with ID " + groupId + " is not currently under Version Control");
+                }
+            },
+            (revision, groupEntity) -> {
+                // set the version control info to null
+                final VersionControlInformationEntity entity = serviceFacade.setVersionControlInformation(requestRevision, groupId, null, null);
+
+                // generate the response
+                return generateOkResponse(entity).build();
+            });
+    }
+
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("process-groups/{id}")
+    @ApiOperation(value = "For a Process Group that is already under Version Control, this will update the version of the flow to a different version",
+        response = VersionControlInformationEntity.class,
+        authorizations = {
+            @Authorization(value = "Read - /process-groups/{uuid}"),
+            @Authorization(value = "Write - /process-groups/{uuid}")
+        })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response updateFlowVersion(@ApiParam("The process group id.") @PathParam("id") final String groupId,
+        @ApiParam(value = "The controller service configuration details.", required = true) final VersionedFlowSnapshotEntity requestEntity) throws IOException, LifecycleManagementException {
+
+        // Verify the request
+        final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+        if (revisionDto == null) {
+            throw new IllegalArgumentException("Process Group Revision must be specified.");
+        }
+
+        final VersionedFlowSnapshot flowSnapshot = requestEntity.getVersionedFlowSnapshot();
+        if (flowSnapshot == null) {
+            throw new IllegalArgumentException("Versioned Flow Snapshot must be supplied.");
+        }
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata();
+        if (snapshotMetadata == null) {
+            throw new IllegalArgumentException("Snapshot Metadata must be supplied.");
+        }
+        if (snapshotMetadata.getBucketIdentifier() == null) {
+            throw new IllegalArgumentException("The Bucket ID must be supplied.");
+        }
+        if (snapshotMetadata.getFlowIdentifier() == null) {
+            throw new IllegalArgumentException("The Flow ID must be supplied.");
+        }
+
+
+        // Perform the request
+        if (isReplicateRequest()) {
+            return replicate(HttpMethod.PUT, requestEntity);
+        }
+
+        // Determine which components will be affected by updating the version
+        final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, NiFiUserUtils.getNiFiUser());
+
+        final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
+        return withWriteLock(
+            serviceFacade,
+            requestEntity,
+            requestRevision,
+            lookup -> {
+                authorizeAffectedComponents(lookup, affectedComponents);
+            },
+            () -> {
+                // We do not enforce that the Process Group is 'not dirty' because at this point,
+                // the client has explicitly indicated the dataflow that the Process Group should
+                // provide and provided the Revision to ensure that they have the most up-to-date
+                // view of the Process Group.
+                serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, false);
+            },
+            (rev, entity) -> {
+                // Update the Process Group to match the proposed flow snapshot
+                final VersionControlInformationDTO versionControlInfoDto = new VersionControlInformationDTO();
+                versionControlInfoDto.setBucketId(snapshotMetadata.getBucketIdentifier());
+                versionControlInfoDto.setCurrent(true);
+                versionControlInfoDto.setFlowId(snapshotMetadata.getFlowIdentifier());
+                versionControlInfoDto.setGroupId(groupId);
+                versionControlInfoDto.setModified(false);
+                versionControlInfoDto.setVersion(snapshotMetadata.getVersion());
+                versionControlInfoDto.setRegistryId(requestEntity.getRegistryId());
+
+                final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroup(rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false);
+                final VersionControlInformationDTO updatedVci = updatedGroup.getComponent().getVersionControlInformation();
+
+                final VersionControlInformationEntity responseEntity = new VersionControlInformationEntity();
+                responseEntity.setProcessGroupRevision(updatedGroup.getRevision());
+                responseEntity.setVersionControlInformation(updatedVci);
+
+                return generateOkResponse(responseEntity).build();
+            });
+    }
+
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("update-requests/{id}")
+    @ApiOperation(value = "Returns the Update Request with the given ID",
+        response = VersionedFlowUpdateRequestEntity.class,
+        authorizations = {
+        })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getUpdateRequest(@ApiParam("The ID of the Update Request") @PathParam("id") final String updateRequestId) {
+        return retrieveRequest("update-requests", updateRequestId);
+    }
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("revert-requests/{id}")
+    @ApiOperation(value = "Returns the Revert Request with the given ID",
+        response = VersionedFlowUpdateRequestEntity.class,
+        authorizations = {
+        })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response getRevertRequest(@ApiParam("The ID of the Revert Request") @PathParam("id") final String revertRequestId) {
+        return retrieveRequest("revert-requests", revertRequestId);
+    }
+
+    private Response retrieveRequest(final String requestType, final String requestId) {
+        if (requestId == null) {
+            throw new IllegalArgumentException("Request ID must be specified.");
+        }
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest = requestManager.getRequest(requestType, requestId, user);
+
+        final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
+        updateRequestDto.setComplete(asyncRequest.isComplete());
+        updateRequestDto.setFailureReason(asyncRequest.getFailureReason());
+        updateRequestDto.setLastUpdated(asyncRequest.getLastUpdated());
+        updateRequestDto.setProcessGroupId(asyncRequest.getProcessGroupId());
+        updateRequestDto.setRequestId(requestId);
+        updateRequestDto.setUri(generateResourceUri("versions", requestType, requestId));
+
+        final RevisionDTO groupRevision = serviceFacade.getProcessGroup(asyncRequest.getProcessGroupId()).getRevision();
+
+        final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
+        updateRequestEntity.setProcessGroupRevision(groupRevision);
+        updateRequestEntity.setRequest(updateRequestDto);
+
+        return generateOkResponse(updateRequestEntity).build();
+    }
+
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("update-requests/{id}")
+    @ApiOperation(value = "Deletes the Update Request with the given ID", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response deleteUpdateRequest(@ApiParam("The ID of the Update Request") @PathParam("id") final String updateRequestId) {
+        return deleteRequest("update-requests", updateRequestId);
+    }
+
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("revert-requests/{id}")
+    @ApiOperation(value = "Deletes the Revert Request with the given ID", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response deleteRevertRequest(@ApiParam("The ID of the Revert Request") @PathParam("id") final String revertRequestId) {
+        return deleteRequest("revert-requests", revertRequestId);
+    }
+
+
+    private Response deleteRequest(final String requestType, final String requestId) {
+        if (requestId == null) {
+            throw new IllegalArgumentException("Request ID must be specified.");
+        }
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest = requestManager.removeRequest(requestType, requestId, user);
+
+        final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
+        updateRequestDto.setComplete(asyncRequest.isComplete());
+        updateRequestDto.setFailureReason(asyncRequest.getFailureReason());
+        updateRequestDto.setLastUpdated(asyncRequest.getLastUpdated());
+        updateRequestDto.setProcessGroupId(asyncRequest.getProcessGroupId());
+        updateRequestDto.setRequestId(requestId);
+        updateRequestDto.setUri(generateResourceUri("versions", requestType, requestId));
+
+        final RevisionDTO groupRevision = serviceFacade.getProcessGroup(asyncRequest.getProcessGroupId()).getRevision();
+
+        final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
+        updateRequestEntity.setProcessGroupRevision(groupRevision);
+        updateRequestEntity.setRequest(updateRequestDto);
+
+        return generateOkResponse(updateRequestEntity).build();
+    }
+
+
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("update-requests/process-groups/{id}")
+    @ApiOperation(value = "For a Process Group that is already under Version Control, this will initiate the action of changing "
+        + "from a specific version of the flow in the Flow Registry to a different version of the flow.", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
+            @Authorization(value = "Read - /process-groups/{uuid}"),
+            @Authorization(value = "Write - /process-groups/{uuid}")
+        })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response initiateVersionControlUpdate(
+        @ApiParam("The process group id.") @PathParam("id") final String groupId,
+        @ApiParam(value = "The controller service configuration details.", required = true) final VersionControlInformationEntity requestEntity) throws IOException {
+
+        // Verify the request
+        final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+        if (revisionDto == null) {
+            throw new IllegalArgumentException("Process Group Revision must be specified");
+        }
+
+        final VersionControlInformationDTO versionControlInfoDto = requestEntity.getVersionControlInformation();
+        if (versionControlInfoDto == null) {
+            throw new IllegalArgumentException("Version Control Information must be supplied.");
+        }
+        if (versionControlInfoDto.getGroupId() == null) {
+            throw new IllegalArgumentException("The Process Group ID must be supplied.");
+        }
+        if (!versionControlInfoDto.getGroupId().equals(groupId)) {
+            throw new IllegalArgumentException("The Process Group ID in the request body does not match the Process Group ID of the requested resource.");
+        }
+        if (versionControlInfoDto.getBucketId() == null) {
+            throw new IllegalArgumentException("The Bucket ID must be supplied.");
+        }
+        if (versionControlInfoDto.getFlowId() == null) {
+            throw new IllegalArgumentException("The Flow ID must be supplied.");
+        }
+        if (versionControlInfoDto.getRegistryId() == null) {
+            throw new IllegalArgumentException("The Registry ID must be supplied.");
+        }
+        if (versionControlInfoDto.getVersion() == null) {
+            throw new IllegalArgumentException("The Version of the flow must be supplied.");
+        }
+
+        // We will perform the updating of the Versioned Flow in a background thread because it can be a long-running process.
+        // In order to do this, we will need some parameters that are only available as Thread-Local variables to the current
+        // thread, so we will gather the values for these parameters up front.
+        final boolean replicateRequest = isReplicateRequest();
+        final ComponentLifecycle componentLifecycle = replicateRequest ? clusterComponentLifecycle : localComponentLifecycle;
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final String idGenerationSeed = getIdGenerationSeed().orElse(null);
+
+
+        // Workflow for this process:
+        // 0. Obtain the versioned flow snapshot to use for the update
+        //    a. Contact registry to download the desired version.
+        //    b. Get Variable Registry of this Process Group and all ancestor groups
+        //    c. Perform diff to find any new variables
+        //    d. Get Variable Registry of any child Process Group in the versioned flow
+        //    e. Perform diff to find any new variables
+        //    f. Prompt user to fill in values for all new variables
+        // 1. Determine which components would be affected (and are enabled/running)
+        //    a. Component itself is modified in some way, other than position changing.
+        //    b. Source and Destination of any Connection that is modified.
+        //    c. Any Processor or Controller Service that references a Controller Service that is modified.
+        // 2. Verify READ and WRITE permissions for user, for every component affected.
+        // 3. Verify that all components in the snapshot exist on all nodes (i.e., the NAR exists)?
+        // 4. Verify that Process Group is already under version control. If not, must start Version Control instead of updateFlow
+        // 5. Verify that Process Group is not 'dirty'.
+        // 6. Stop all Processors, Funnels, Ports that are affected.
+        // 7. Wait for all of the components to finish stopping.
+        // 8. Disable all Controller Services that are affected.
+        // 9. Wait for all Controller Services to finish disabling.
+        // 10. Ensure that if any connection was deleted, that it has no data in it. Ensure that no Input Port
+        //    was removed, unless it currently has no incoming connections. Ensure that no Output Port was removed,
+        //    unless it currently has no outgoing connections. Checking ports & connections could be done before
+        //    stopping everything, but removal of Connections cannot.
+        // 11. Update variable registry to include new variables
+        //    (only new variables so don't have to worry about affected components? Or do we need to in case a processor
+        //    is already referencing the variable? In which case we need to include the affected components above in the
+        //    Set of affected components before stopping/disabling.).
+        // 12. Update components in the Process Group; update Version Control Information.
+        // 13. Re-Enable all affected Controller Services that were not removed.
+        // 14. Re-Start all Processors, Funnels, Ports that are affected and not removed.
+
+        // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
+        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation());
+
+        // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
+        // the flow snapshot to contain compatible bundles.
+        discoverCompatibleBundles(flowSnapshot.getFlowContents());
+
+        // Step 1: Determine which components will be affected by updating the version
+        final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, user);
+
+        final URI exampleUri = getAbsolutePath();
+        final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
+        return withWriteLock(
+            serviceFacade,
+            requestEntity,
+            requestRevision,
+            lookup -> {
+                // Step 2: Verify READ and WRITE permissions for user, for every component affected.
+                authorizeAffectedComponents(lookup, affectedComponents);
+            },
+            () -> {
+                // Step 3: Verify that all components in the snapshot exist on all nodes
+                // Step 4: Verify that Process Group is already under version control. If not, must start Version Control instead of updating flow
+                // Step 5: Verify that Process Group is not 'dirty'
+                serviceFacade.verifyCanUpdate(groupId, flowSnapshot, false, true);
+            },
+            (revision, processGroupEntity) -> {
+                // Create an asynchronous request that will occur in the background, because this request may
+                // result in stopping components, which can take an indeterminate amount of time.
+                final String requestId = UUID.randomUUID().toString();
+                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user);
+
+                // Submit the request to be performed in the background
+                final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
+                    try {
+                        final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
+                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, true);
+
+                        vcur.markComplete(updatedVersionControlEntity);
+                    } catch (final LifecycleManagementException e) {
+                        logger.error("Failed to update flow to new version", e);
+                        vcur.setFailureReason("Failed to update flow to new version due to " + e);
+                    }
+                };
+
+                requestManager.submitRequest("update-requests", requestId, request, updateTask);
+
+                // Generate the response.
+                final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
+                updateRequestDto.setComplete(request.isComplete());
+                updateRequestDto.setFailureReason(request.getFailureReason());
+                updateRequestDto.setLastUpdated(request.getLastUpdated());
+                updateRequestDto.setProcessGroupId(groupId);
+                updateRequestDto.setRequestId(requestId);
+                updateRequestDto.setUri(generateResourceUri("versions", "update-requests", requestId));
+
+                final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
+                final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revision);
+                updateRequestEntity.setProcessGroupRevision(groupRevision);
+                updateRequestEntity.setRequest(updateRequestDto);
+
+                return generateOkResponse(updateRequestEntity).build();
+            });
+    }
+
+
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("revert-requests/process-groups/{id}")
+    @ApiOperation(value = "For a Process Group that is already under Version Control, this will initiate the action of reverting "
+        + "any changes that have been made to the Process Group since it was last synchronized with the Flow Registry. This will result in the "
+        + "flow matching the Versioned Flow that exists in the Flow Registry.", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
+            @Authorization(value = "Read - /process-groups/{uuid}"),
+            @Authorization(value = "Write - /process-groups/{uuid}")
+        })
+    @ApiResponses(value = {
+        @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+        @ApiResponse(code = 401, message = "Client could not be authenticated."),
+        @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+        @ApiResponse(code = 404, message = "The specified resource could not be found."),
+        @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+    })
+    public Response initiateRevertFlowVersion(@ApiParam("The process group id.") @PathParam("id") final String groupId,
+        @ApiParam(value = "The controller service configuration details.", required = true) final VersionControlInformationEntity requestEntity) throws IOException {
+
+        // Verify the request
+        final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+        if (revisionDto == null) {
+            throw new IllegalArgumentException("Process Group Revision must be specified");
+        }
+
+        final VersionControlInformationDTO versionControlInfoDto = requestEntity.getVersionControlInformation();
+        if (versionControlInfoDto == null) {
+            throw new IllegalArgumentException("Version Control Information must be supplied.");
+        }
+        if (versionControlInfoDto.getGroupId() == null) {
+            throw new IllegalArgumentException("The Process Group ID must be supplied.");
+        }
+        if (!versionControlInfoDto.getGroupId().equals(groupId)) {
+            throw new IllegalArgumentException("The Process Group ID in the request body does not match the Process Group ID of the requested resource.");
+        }
+        if (versionControlInfoDto.getBucketId() == null) {
+            throw new IllegalArgumentException("The Bucket ID must be supplied.");
+        }
+        if (versionControlInfoDto.getFlowId() == null) {
+            throw new IllegalArgumentException("The Flow ID must be supplied.");
+        }
+        if (versionControlInfoDto.getRegistryId() == null) {
+            throw new IllegalArgumentException("The Registry ID must be supplied.");
+        }
+        if (versionControlInfoDto.getVersion() == null) {
+            throw new IllegalArgumentException("The Version of the flow must be supplied.");
+        }
+
+        // We will perform the updating of the Versioned Flow in a background thread because it can be a long-running process.
+        // In order to do this, we will need some parameters that are only available as Thread-Local variables to the current
+        // thread, so we will gather the values for these parameters up front.
+        final boolean replicateRequest = isReplicateRequest();
+        final ComponentLifecycle componentLifecycle = replicateRequest ? clusterComponentLifecycle : localComponentLifecycle;
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final String idGenerationSeed = getIdGenerationSeed().orElse(null);
+
+        // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
+        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation());
+
+        // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
+        // the flow snapshot to contain compatible bundles.
+        discoverCompatibleBundles(flowSnapshot.getFlowContents());
+
+        // Step 1: Determine which components will be affected by updating the version
+        final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, user);
+
+        final URI exampleUri = getAbsolutePath();
+        final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
+        return withWriteLock(
+            serviceFacade,
+            requestEntity,
+            requestRevision,
+            lookup -> {
+                // Step 2: Verify READ and WRITE permissions for user, for every component affected.
+                authorizeAffectedComponents(lookup, affectedComponents);
+            },
+            () -> {
+                // Step 3: Verify that all components in the snapshot exist on all nodes
+                // Step 4: Verify that Process Group is already under version control. If not, must start Version Control instead of updating flow
+                // Step 5: Verify that Process Group is not 'dirty'
+                serviceFacade.verifyCanUpdate(groupId, flowSnapshot, false, false);
+            },
+            (revision, processGroupEntity) -> {
+                // Ensure that the information passed in is correct
+                final VersionControlInformationEntity currentVersionEntity = serviceFacade.getVersionControlInformation(groupId);
+                if (currentVersionEntity == null) {
+                    throw new IllegalStateException("Process Group cannot be reverted to the previous version of the flow because Process Group is not under Version Control.");
+                }
+
+                final VersionControlInformationDTO currentVersion = currentVersionEntity.getVersionControlInformation();
+                if (!currentVersion.getBucketId().equals(versionControlInfoDto.getBucketId())) {
+                    throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
+                }
+                if (!currentVersion.getFlowId().equals(versionControlInfoDto.getFlowId())) {
+                    throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
+                }
+                if (!currentVersion.getRegistryId().equals(versionControlInfoDto.getRegistryId())) {
+                    throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
+                }
+                if (!currentVersion.getVersion().equals(versionControlInfoDto.getVersion())) {
+                    throw new IllegalArgumentException("The Version Control Information provided does not match the flow that the Process Group is currently synchronized with.");
+                }
+
+                // If the information passed in is correct, but there have been no changes, there is nothing to do - just register the request, mark it complete, and return.
+                if (currentVersion.getModified() == Boolean.FALSE) {
+                    final String requestId = UUID.randomUUID().toString();
+                    final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user);
+                    requestManager.submitRequest("revert-requests", requestId, request, task -> {
+                    });
+
+                    // There is nothing to do. Generate the response and send it back to the user.
+                    final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
+                    updateRequestDto.setComplete(true);
+                    updateRequestDto.setFailureReason(null);
+                    updateRequestDto.setLastUpdated(new Date());
+                    updateRequestDto.setProcessGroupId(groupId);
+                    updateRequestDto.setRequestId(requestId);
+                    updateRequestDto.setUri(generateResourceUri("versions", "update-requests", requestId));
+
+                    final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
+                    updateRequestEntity.setProcessGroupRevision(revisionDto);
+                    updateRequestEntity.setRequest(updateRequestDto);
+
+                    return generateOkResponse(updateRequestEntity).build();
+                }
+
+
+                // Create an asynchronous request that will occur in the background, because this request may
+                // result in stopping components, which can take an indeterminate amount of time.
+                final String requestId = UUID.randomUUID().toString();
+                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user);
+
+                // Submit the request to be performed in the background
+                final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
+                    try {
+                        // TODO: change the URI to the new endpoint for 'revert' instead of 'change version'
+                        final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
+                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false);
+
+                        vcur.markComplete(updatedVersionControlEntity);
+                    } catch (final LifecycleManagementException e) {
+                        logger.error("Failed to update flow to new version", e);
+                        vcur.setFailureReason("Failed to update flow to new version due to " + e);
+                    }
+                };
+
+                requestManager.submitRequest("revert-requests", requestId, request, updateTask);
+
+                // Generate the response.
+                final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
+                updateRequestDto.setComplete(request.isComplete());
+                updateRequestDto.setFailureReason(request.getFailureReason());
+                updateRequestDto.setLastUpdated(request.getLastUpdated());
+                updateRequestDto.setProcessGroupId(groupId);
+                updateRequestDto.setRequestId(requestId);
+                updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
+
+                final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
+                final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revision);
+                updateRequestEntity.setProcessGroupRevision(groupRevision);
+                updateRequestEntity.setRequest(updateRequestDto);
+
+                return generateOkResponse(updateRequestEntity).build();
+            });
+    }
+
+    private VersionControlInformationEntity updateFlowVersion(final String groupId, final ComponentLifecycle componentLifecycle, final URI exampleUri,
+        final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user, final boolean replicateRequest, final VersionControlInformationEntity requestEntity,
+        final VersionedFlowSnapshot flowSnapshot, final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest, final String idGenerationSeed,
+        final boolean verifyNotModified) throws LifecycleManagementException {
+
+        // Steps 6-7: Determine which components must be stopped and stop them.
+        // Do we need to stop other types? Input Ports, Output Ports, Funnels, RPGs, etc.
+        final Set<String> stoppableReferenceTypes = new HashSet<>();
+        stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
+        stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_REMOTE_INPUT_PORT);
+        stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_REMOTE_OUTPUT_PORT);
+
+        final Set<AffectedComponentEntity> runningComponents = affectedComponents.stream()
+            .filter(dto -> stoppableReferenceTypes.contains(dto.getComponent().getReferenceType()))
+            .filter(dto -> "Running".equalsIgnoreCase(dto.getComponent().getState()))
+            .collect(Collectors.toSet());
+
+        logger.info("Stopping {} Processors", runningComponents.size());
+        final Pause stopComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        componentLifecycle.scheduleComponents(exampleUri, user, groupId, runningComponents, ScheduledState.STOPPED, stopComponentsPause);
+        asyncRequest.setLastUpdated(new Date());
+
+        // Steps 8-9. Disable enabled controller services that are affected
+        final Set<AffectedComponentEntity> enabledServices = affectedComponents.stream()
+            .filter(dto -> AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE.equals(dto.getComponent().getReferenceType()))
+            .filter(dto -> "Enabled".equalsIgnoreCase(dto.getComponent().getState()))
+            .collect(Collectors.toSet());
+
+        logger.info("Disabling {} Controller Services", enabledServices.size());
+        final Pause disableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        componentLifecycle.activateControllerServices(exampleUri, user, groupId, enabledServices, ControllerServiceState.DISABLED, disableServicesPause);
+        asyncRequest.setLastUpdated(new Date());
+
+        logger.info("Updating Process Group with ID {} to version {} of the Versioned Flow", groupId, flowSnapshot.getSnapshotMetadata().getVersion());
+        // If replicating request, steps 10-12 are performed on each node individually, and this is accomplished
+        // by replicating a PUT to /nifi-api/versions/process-groups/{groupId}
+        if (replicateRequest) {
+
+            final URI updateUri;
+            try {
+                updateUri = new URI(exampleUri.getScheme(), exampleUri.getUserInfo(), exampleUri.getHost(),
+                    exampleUri.getPort(), "/nifi-api/versions/process-groups/" + groupId, null, exampleUri.getFragment());
+            } catch (URISyntaxException e) {
+                throw new RuntimeException(e);
+            }
+
+            final Map<String, String> headers = new HashMap<>();
+            headers.put("content-type", MediaType.APPLICATION_JSON);
+
+            final VersionedFlowSnapshotEntity snapshotEntity = new VersionedFlowSnapshotEntity();
+            snapshotEntity.setProcessGroupRevision(requestEntity.getProcessGroupRevision());
+            snapshotEntity.setRegistryId(requestEntity.getVersionControlInformation().getRegistryId());
+            snapshotEntity.setVersionedFlow(flowSnapshot);
+
+            final NodeResponse clusterResponse;
+            try {
+                if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                    clusterResponse = getRequestReplicator().replicate(user, HttpMethod.PUT, updateUri, snapshotEntity, headers).awaitMergedResponse();
+                } else {
+                    clusterResponse = getRequestReplicator().forwardToCoordinator(
+                        getClusterCoordinatorNode(), user, HttpMethod.PUT, updateUri, snapshotEntity, headers).awaitMergedResponse();
+                }
+            } catch (final InterruptedException ie) {
+                Thread.currentThread().interrupt();
+                throw new LifecycleManagementException("Interrupted while updating flows across cluster", ie);
+            }
+
+            final int disableServicesStatus = clusterResponse.getStatus();
+            if (disableServicesStatus != Status.OK.getStatusCode()) {
+                final String explanation = getResponseEntity(clusterResponse, String.class);
+                throw new LifecycleManagementException("Failed to update Flow on all nodes in cluster due to " + explanation);
+            }
+
+        } else {
+            // Step 10: Ensure that if any connection exists in the flow and does not exist in the proposed snapshot,
+            // that it has no data in it. Ensure that no Input Port was removed, unless it currently has no incoming connections.
+            // Ensure that no Output Port was removed, unless it currently has no outgoing connections.
+            serviceFacade.verifyCanUpdate(groupId, flowSnapshot, true, verifyNotModified);
+
+            // Step 11-12. Update Process Group to the new flow and update variable registry with any Variables that were added or removed
+            final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
+            final Revision revision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
+            final VersionControlInformationDTO vci = requestEntity.getVersionControlInformation();
+            serviceFacade.updateProcessGroup(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified);
+        }
+
+        asyncRequest.setLastUpdated(new Date());
+
+        // Step 13. Re-enable all disabled controller services
+        final Pause enableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        final Set<AffectedComponentEntity> servicesToEnable = getUpdatedEntities(enabledServices, user);
+        logger.info("Successfully updated flow; re-enabling {} Controller Services", servicesToEnable.size());
+        componentLifecycle.activateControllerServices(exampleUri, user, groupId, servicesToEnable, ControllerServiceState.ENABLED, enableServicesPause);
+        asyncRequest.setLastUpdated(new Date());
+
+        // Step 14. Restart all components
+        final Set<AffectedComponentEntity> componentsToStart = getUpdatedEntities(runningComponents, user);
+        final Pause startComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        logger.info("Restarting {} Processors", componentsToStart.size());
+        componentLifecycle.scheduleComponents(exampleUri, user, groupId, componentsToStart, ScheduledState.RUNNING, startComponentsPause);
+        asyncRequest.setLastUpdated(new Date());
+
+        return serviceFacade.getVersionControlInformation(groupId);
+    }
+
+    /**
+     * Extracts the response entity from the specified node response.
+     *
+     * @param nodeResponse node response
+     * @param clazz class
+     * @param <T> type of class
+     * @return the response entity
+     */
+    @SuppressWarnings("unchecked")
+    private <T> T getResponseEntity(final NodeResponse nodeResponse, final Class<T> clazz) {
+        T entity = (T) nodeResponse.getUpdatedEntity();
+        if (entity == null) {
+            entity = nodeResponse.getClientResponse().getEntity(clazz);
+        }
+        return entity;
+    }
+
+
+    private void authorizeAffectedComponents(final AuthorizableLookup lookup, final Set<AffectedComponentEntity> affectedComponents) {
+        final Map<String, List<AffectedComponentEntity>> componentsByType = affectedComponents.stream()
+            .collect(Collectors.groupingBy(entity -> entity.getComponent().getReferenceType()));
+
+        authorize(componentsByType.get(ComponentType.PROCESSOR.name()), id -> lookup.getProcessor(id).getAuthorizable());
+        authorize(componentsByType.get(ComponentType.CONTROLLER_SERVICE.name()), id -> lookup.getControllerService(id).getAuthorizable());
+
+        authorize(componentsByType.get(ComponentType.CONNECTION.name()), id -> lookup.getConnection(id).getAuthorizable());
+        authorize(componentsByType.get(ComponentType.FUNNEL.name()), id -> lookup.getFunnel(id));
+        authorize(componentsByType.get(ComponentType.INPUT_PORT.name()), id -> lookup.getInputPort(id));
+        authorize(componentsByType.get(ComponentType.OUTPUT_PORT.name()), id -> lookup.getOutputPort(id));
+        authorize(componentsByType.get(ComponentType.LABEL.name()), id -> lookup.getLabel(id));
+
+        authorize(componentsByType.get(ComponentType.PROCESS_GROUP.name()), id -> lookup.getProcessGroup(id).getAuthorizable());
+        authorize(componentsByType.get(ComponentType.REMOTE_PROCESS_GROUP.name()), id -> lookup.getRemoteProcessGroup(id));
+
+
+        // Remote Input Ports and Remote Output Ports are not authorized independently but rather at the Remote Process Group level,
+        // so we have to treat these a little differently.
+        componentsByType.getOrDefault(ComponentType.REMOTE_INPUT_PORT.name(), Collections.emptyList()).stream()
+            .forEach(affectedPort -> {
+                final String rpgId = affectedPort.getComponent().getProcessGroupId();
+                final Authorizable rpg = lookup.getRemoteProcessGroup(rpgId);
+                rpg.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                rpg.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+            });
+
+        componentsByType.getOrDefault(ComponentType.REMOTE_OUTPUT_PORT.name(), Collections.emptyList()).stream()
+            .forEach(affectedPort -> {
+                final String rpgId = affectedPort.getComponent().getProcessGroupId();
+                final Authorizable rpg = lookup.getRemoteProcessGroup(rpgId);
+                rpg.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                rpg.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+            });
+    }
+
+    private Set<AffectedComponentEntity> getUpdatedEntities(final Set<AffectedComponentEntity> originalEntities, final NiFiUser user) {
+        final Set<AffectedComponentEntity> entities = new LinkedHashSet<>();
+
+        for (final AffectedComponentEntity original : originalEntities) {
+            try {
+                final AffectedComponentEntity updatedEntity = AffectedComponentUtils.updateEntity(original, serviceFacade, dtoFactory, user);
+                entities.add(updatedEntity);
+            } catch (final ResourceNotFoundException rnfe) {
+                // Component was removed. Just continue on without adding anything to the entities.
+                // We do this because the intent is to get updated versions of the entities with current
+                // Revisions so that we can change the states of the components. If the component was removed,
+                // then we can just drop the entity, since there is no need to change its state.
+            }
+        }
+
+        return entities;
+    }
+
+
+    private void authorize(final List<AffectedComponentEntity> componentDtos, final Function<String, Authorizable> authFunction) {
+        if (componentDtos != null) {
+            for (final AffectedComponentEntity entity : componentDtos) {
+                final Authorizable authorizable = authFunction.apply(entity.getComponent().getId());
+                authorizable.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                authorizable.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+            }
+        }
+    }
+
+
+    public void setServiceFacade(NiFiServiceFacade serviceFacade) {
+        this.serviceFacade = serviceFacade;
+    }
+
+    public void setAuthorizer(Authorizer authorizer) {
+        this.authorizer = authorizer;
+    }
+
+    public void setClusterComponentLifecycle(ComponentLifecycle componentLifecycle) {
+        this.clusterComponentLifecycle = componentLifecycle;
+    }
+
+    public void setLocalComponentLifecycle(ComponentLifecycle componentLifecycle) {
+        this.localComponentLifecycle = componentLifecycle;
+    }
+
+    public void setDtoFactory(final DtoFactory dtoFactory) {
+        this.dtoFactory = dtoFactory;
+    }
+
+    private BundleDTO createBundleDto(final Bundle bundle) {
+        final BundleDTO dto = new BundleDTO();
+        dto.setArtifact(bundle.getArtifact());
+        dto.setGroup(dto.getGroup());
+        dto.setVersion(dto.getVersion());
+        return dto;
+    }
+
+    /**
+     * Discovers the compatible bundle details for the components in the specified snippet.
+     *
+     * @param versionedGroup the versioned group
+     */
+    private void discoverCompatibleBundles(final VersionedProcessGroup versionedGroup) {
+        if (versionedGroup.getProcessors() != null) {
+            versionedGroup.getProcessors().forEach(processor -> {
+                final BundleCoordinate coordinate = BundleUtils.getCompatibleBundle(processor.getType(), createBundleDto(processor.getBundle()));
+
+                final Bundle bundle = new Bundle();
+                bundle.setArtifact(coordinate.getId());
+                bundle.setGroup(coordinate.getGroup());
+                bundle.setVersion(coordinate.getVersion());
+                processor.setBundle(bundle);
+            });
+        }
+
+        if (versionedGroup.getControllerServices() != null) {
+            versionedGroup.getControllerServices().forEach(controllerService -> {
+                final BundleCoordinate coordinate = BundleUtils.getCompatibleBundle(controllerService.getType(), createBundleDto(controllerService.getBundle()));
+
+                final Bundle bundle = new Bundle();
+                bundle.setArtifact(coordinate.getId());
+                bundle.setGroup(coordinate.getGroup());
+                bundle.setVersion(coordinate.getVersion());
+                controllerService.setBundle(bundle);
+            });
+        }
+
+        if (versionedGroup.getProcessGroups() != null) {
+            versionedGroup.getProcessGroups().forEach(processGroup -> {
+                discoverCompatibleBundles(processGroup);
+            });
+        }
+    }
+
+    private static class ActiveRequest {
+        private static final long MAX_REQUEST_LOCK_NANOS = TimeUnit.MINUTES.toNanos(1L);
+
+        private final String requestId;
+        private final long creationNanos = System.nanoTime();
+        private final long expirationTime = creationNanos + MAX_REQUEST_LOCK_NANOS;
+
+        private ActiveRequest(final String requestId) {
+            this.requestId = requestId;
+        }
+
+        public boolean isExpired() {
+            return System.nanoTime() > expirationTime;
+        }
+
+        public String getRequestId() {
+            return requestId;
+        }
+    }
+}


[23/50] nifi git commit: NIFI-4436: Integrate with actual Flow Registry via REST Client - Store Bucket Name, Flow Name, Flow Description for VersionControlInformation - Added endpoint for determining local modifications to a process group - Updated autho

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index b010bf3..b808ae6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,15 +17,40 @@
 
 package org.apache.nifi.web.api;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
 import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.ProcessGroupAuthorizable;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.user.NiFiUser;
@@ -34,7 +59,6 @@ import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
-import org.apache.nifi.registry.flow.ComponentType;
 import org.apache.nifi.registry.flow.FlowRegistryUtils;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
@@ -55,9 +79,9 @@ import org.apache.nifi.web.api.dto.VersionedFlowDTO;
 import org.apache.nifi.web.api.dto.VersionedFlowUpdateRequestDTO;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionControlComponentMappingEntity;
 import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
-import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowUpdateRequestEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
@@ -70,37 +94,12 @@ import org.apache.nifi.web.util.Pause;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 
 @Path("/versions")
 @Api(value = "/versions", description = "Endpoint for managing version control for a flow")
@@ -356,7 +355,10 @@ public class VersionsResource extends ApplicationResource {
             response = VersionControlInformationEntity.class,
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
-                @Authorization(value = "Read - /process-groups/{uuid}")
+                @Authorization(value = "Read - /process-groups/{uuid}"),
+                @Authorization(value = "Write - /process-groups/{uuid}"),
+                @Authorization(value = "Read - /{component-type}/{uuid} - For all encapsulated components"),
+                @Authorization(value = "Read - any referenced Controller Services by any encapsulated components - /controller-services/{uuid}")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -500,9 +502,11 @@ public class VersionsResource extends ApplicationResource {
             requestEntity,
             groupRevision,
             lookup -> {
-                final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
+                final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
+                final Authorizable processGroup = groupAuthorizable.getAuthorizable();
                 processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
                 processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
             },
             () -> {
                 final VersionControlInformationEntity entity = serviceFacade.getVersionControlInformation(groupId);
@@ -663,22 +667,21 @@ public class VersionsResource extends ApplicationResource {
             throw new IllegalArgumentException("The Flow ID must be supplied.");
         }
 
-
         // Perform the request
         if (isReplicateRequest()) {
             return replicate(HttpMethod.PUT, requestEntity);
         }
 
-        // Determine which components will be affected by updating the version
-        final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, NiFiUserUtils.getNiFiUser());
-
         final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
         return withWriteLock(
             serviceFacade,
             requestEntity,
             requestRevision,
             lookup -> {
-                authorizeAffectedComponents(lookup, affectedComponents);
+                final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
+                final Authorizable processGroup = groupAuthorizable.getAuthorizable();
+                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
             },
             () -> {
                 // We do not enforce that the Process Group is 'not dirty' because at this point,
@@ -691,13 +694,16 @@ public class VersionsResource extends ApplicationResource {
                 // Update the Process Group to match the proposed flow snapshot
                 final VersionControlInformationDTO versionControlInfoDto = new VersionControlInformationDTO();
                 versionControlInfoDto.setBucketId(snapshotMetadata.getBucketIdentifier());
+                versionControlInfoDto.setBucketName(snapshotMetadata.getBucketName());
                 versionControlInfoDto.setCurrent(true);
                 versionControlInfoDto.setFlowId(snapshotMetadata.getFlowIdentifier());
                 versionControlInfoDto.setFlowName(snapshotMetadata.getFlowName());
+                versionControlInfoDto.setFlowDescription(snapshotMetadata.getFlowDescription());
                 versionControlInfoDto.setGroupId(groupId);
                 versionControlInfoDto.setModified(false);
                 versionControlInfoDto.setVersion(snapshotMetadata.getVersion());
                 versionControlInfoDto.setRegistryId(requestEntity.getRegistryId());
+                versionControlInfoDto.setRegistryName(serviceFacade.getFlowRegistryName(requestEntity.getRegistryId()));
 
                 final ProcessGroupEntity updatedGroup = serviceFacade.updateProcessGroup(rev, groupId, versionControlInfoDto, flowSnapshot, getIdGenerationSeed().orElse(null), false);
                 final VersionControlInformationDTO updatedVci = updatedGroup.getComponent().getVersionControlInformation();
@@ -769,6 +775,13 @@ public class VersionsResource extends ApplicationResource {
         updateRequestDto.setProcessGroupId(asyncRequest.getProcessGroupId());
         updateRequestDto.setRequestId(requestId);
         updateRequestDto.setUri(generateResourceUri("versions", requestType, requestId));
+        updateRequestDto.setState(asyncRequest.getState());
+        updateRequestDto.setPercentComplete(asyncRequest.getPercentComplete());
+
+        if (updateRequestDto.isComplete()) {
+            final VersionControlInformationEntity vciEntity = serviceFacade.getVersionControlInformation(asyncRequest.getProcessGroupId());
+            updateRequestDto.setVersionControlInformation(vciEntity == null ? null : vciEntity.getVersionControlInformation());
+        }
 
         final RevisionDTO groupRevision = serviceFacade.getProcessGroup(asyncRequest.getProcessGroupId()).getRevision();
 
@@ -830,6 +843,13 @@ public class VersionsResource extends ApplicationResource {
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
         final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest = requestManager.removeRequest(requestType, requestId, user);
+        if (asyncRequest == null) {
+            throw new ResourceNotFoundException("Could not find request of type " + requestType + " with ID " + requestId);
+        }
+
+        if (!asyncRequest.isComplete()) {
+            asyncRequest.cancel();
+        }
 
         final VersionedFlowUpdateRequestDTO updateRequestDto = new VersionedFlowUpdateRequestDTO();
         updateRequestDto.setComplete(asyncRequest.isComplete());
@@ -838,6 +858,13 @@ public class VersionsResource extends ApplicationResource {
         updateRequestDto.setProcessGroupId(asyncRequest.getProcessGroupId());
         updateRequestDto.setRequestId(requestId);
         updateRequestDto.setUri(generateResourceUri("versions", requestType, requestId));
+        updateRequestDto.setPercentComplete(asyncRequest.getPercentComplete());
+        updateRequestDto.setState(asyncRequest.getState());
+
+        if (updateRequestDto.isComplete()) {
+            final VersionControlInformationEntity vciEntity = serviceFacade.getVersionControlInformation(asyncRequest.getProcessGroupId());
+            updateRequestDto.setVersionControlInformation(vciEntity == null ? null : vciEntity.getVersionControlInformation());
+        }
 
         final RevisionDTO groupRevision = serviceFacade.getProcessGroup(asyncRequest.getProcessGroupId()).getRevision();
 
@@ -861,7 +888,10 @@ public class VersionsResource extends ApplicationResource {
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
                 @Authorization(value = "Read - /process-groups/{uuid}"),
-                @Authorization(value = "Write - /process-groups/{uuid}")
+                @Authorization(value = "Write - /process-groups/{uuid}"),
+                @Authorization(value = "Read - /{component-type}/{uuid} - For all encapsulated components"),
+                @Authorization(value = "Write - /{component-type}/{uuid} - For all encapsulated components"),
+                @Authorization(value = "Write - if the template contains any restricted components - /restricted-components")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -924,7 +954,7 @@ public class VersionsResource extends ApplicationResource {
         //    a. Component itself is modified in some way, other than position changing.
         //    b. Source and Destination of any Connection that is modified.
         //    c. Any Processor or Controller Service that references a Controller Service that is modified.
-        // 2. Verify READ and WRITE permissions for user, for every component affected.
+        // 2. Verify READ and WRITE permissions for user, for every component.
         // 3. Verify that all components in the snapshot exist on all nodes (i.e., the NAR exists)?
         // 4. Verify that Process Group is already under version control. If not, must start Version Control instead of updateFlow
         // 5. Verify that Process Group is not 'dirty'.
@@ -961,8 +991,13 @@ public class VersionsResource extends ApplicationResource {
             requestEntity,
             requestRevision,
             lookup -> {
-                // Step 2: Verify READ and WRITE permissions for user, for every component affected.
-                authorizeAffectedComponents(lookup, affectedComponents);
+                // Step 2: Verify READ and WRITE permissions for user, for every component.
+                final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
+                final Authorizable processGroup = groupAuthorizable.getAuthorizable();
+                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
+                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.WRITE, true, false, true, true);
 
                 final VersionedProcessGroup groupContents = flowSnapshot.getFlowContents();
                 final boolean containsRestrictedComponents = FlowRegistryUtils.containsRestrictedComponent(groupContents);
@@ -980,7 +1015,7 @@ public class VersionsResource extends ApplicationResource {
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
-                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user);
+                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Stopping Processors");
 
                 // Submit the request to be performed in the background
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
@@ -1005,6 +1040,8 @@ public class VersionsResource extends ApplicationResource {
                 updateRequestDto.setProcessGroupId(groupId);
                 updateRequestDto.setRequestId(requestId);
                 updateRequestDto.setUri(generateResourceUri("versions", "update-requests", requestId));
+                updateRequestDto.setPercentComplete(request.getPercentComplete());
+                updateRequestDto.setState(request.getState());
 
                 final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
                 final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revision);
@@ -1029,7 +1066,10 @@ public class VersionsResource extends ApplicationResource {
             notes = NON_GUARANTEED_ENDPOINT,
             authorizations = {
                 @Authorization(value = "Read - /process-groups/{uuid}"),
-                @Authorization(value = "Write - /process-groups/{uuid}")
+                @Authorization(value = "Write - /process-groups/{uuid}"),
+                @Authorization(value = "Read - /{component-type}/{uuid} - For all encapsulated components"),
+                @Authorization(value = "Write - /{component-type}/{uuid} - For all encapsulated components"),
+                @Authorization(value = "Write - if the template contains any restricted components - /restricted-components")
             })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@@ -1095,8 +1135,13 @@ public class VersionsResource extends ApplicationResource {
             requestEntity,
             requestRevision,
             lookup -> {
-                // Step 2: Verify READ and WRITE permissions for user, for every component affected.
-                authorizeAffectedComponents(lookup, affectedComponents);
+                // Step 2: Verify READ and WRITE permissions for user, for every component.
+                final ProcessGroupAuthorizable groupAuthorizable = lookup.getProcessGroup(groupId);
+                final Authorizable processGroup = groupAuthorizable.getAuthorizable();
+                processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.READ, true, false, true, true);
+                super.authorizeProcessGroup(groupAuthorizable, authorizer, lookup, RequestAction.WRITE, true, false, true, true);
 
                 final VersionedProcessGroup groupContents = flowSnapshot.getFlowContents();
                 final boolean containsRestrictedComponents = FlowRegistryUtils.containsRestrictedComponent(groupContents);
@@ -1134,7 +1179,7 @@ public class VersionsResource extends ApplicationResource {
                 // If the information passed in is correct, but there have been no changes, there is nothing to do - just register the request, mark it complete, and return.
                 if (currentVersion.getModified() == Boolean.FALSE) {
                     final String requestId = UUID.randomUUID().toString();
-                    final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user);
+                    final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Complete");
                     requestManager.submitRequest("revert-requests", requestId, request, task -> {
                     });
 
@@ -1145,7 +1190,10 @@ public class VersionsResource extends ApplicationResource {
                     updateRequestDto.setLastUpdated(new Date());
                     updateRequestDto.setProcessGroupId(groupId);
                     updateRequestDto.setRequestId(requestId);
+                    updateRequestDto.setVersionControlInformation(currentVersion);
                     updateRequestDto.setUri(generateResourceUri("versions", "revert-requests", requestId));
+                    updateRequestDto.setPercentComplete(100);
+                    updateRequestDto.setState(request.getState());
 
                     final VersionedFlowUpdateRequestEntity updateRequestEntity = new VersionedFlowUpdateRequestEntity();
                     updateRequestEntity.setProcessGroupRevision(revisionDto);
@@ -1159,19 +1207,18 @@ public class VersionsResource extends ApplicationResource {
                 // Create an asynchronous request that will occur in the background, because this request may
                 // result in stopping components, which can take an indeterminate amount of time.
                 final String requestId = UUID.randomUUID().toString();
-                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user);
+                final AsynchronousWebRequest<VersionControlInformationEntity> request = new StandardAsynchronousWebRequest<>(requestId, groupId, user, "Stopping Processors");
 
                 // Submit the request to be performed in the background
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
-                        // TODO: change the URI to the new endpoint for 'revert' instead of 'change version'
                         final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
                             affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final LifecycleManagementException e) {
                         logger.error("Failed to update flow to new version", e);
-                        vcur.setFailureReason("Failed to update flow to new version due to " + e);
+                        vcur.setFailureReason("Failed to update flow to new version due to " + e.getMessage());
                     }
                 };
 
@@ -1201,7 +1248,6 @@ public class VersionsResource extends ApplicationResource {
         final boolean verifyNotModified) throws LifecycleManagementException {
 
         // Steps 6-7: Determine which components must be stopped and stop them.
-        // Do we need to stop other types? Input Ports, Output Ports, Funnels, RPGs, etc.
         final Set<String> stoppableReferenceTypes = new HashSet<>();
         stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
         stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_REMOTE_INPUT_PORT);
@@ -1215,7 +1261,11 @@ public class VersionsResource extends ApplicationResource {
         logger.info("Stopping {} Processors", runningComponents.size());
         final Pause stopComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
         componentLifecycle.scheduleComponents(exampleUri, user, groupId, runningComponents, ScheduledState.STOPPED, stopComponentsPause);
-        asyncRequest.setLastUpdated(new Date());
+
+        if (asyncRequest.isCancelled()) {
+            return null;
+        }
+        asyncRequest.update(new Date(), "Disabling Affected Controller Services", 20);
 
         // Steps 8-9. Disable enabled controller services that are affected
         final Set<AffectedComponentEntity> enabledServices = affectedComponents.stream()
@@ -1226,7 +1276,11 @@ public class VersionsResource extends ApplicationResource {
         logger.info("Disabling {} Controller Services", enabledServices.size());
         final Pause disableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
         componentLifecycle.activateControllerServices(exampleUri, user, groupId, enabledServices, ControllerServiceState.DISABLED, disableServicesPause);
-        asyncRequest.setLastUpdated(new Date());
+
+        if (asyncRequest.isCancelled()) {
+            return null;
+        }
+        asyncRequest.update(new Date(), "Updating Flow", 40);
 
         logger.info("Updating Process Group with ID {} to version {} of the Versioned Flow", groupId, flowSnapshot.getSnapshotMetadata().getVersion());
         // If replicating request, steps 10-12 are performed on each node individually, and this is accomplished
@@ -1281,21 +1335,32 @@ public class VersionsResource extends ApplicationResource {
             serviceFacade.updateProcessGroupContents(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified, false);
         }
 
-        asyncRequest.setLastUpdated(new Date());
+        if (asyncRequest.isCancelled()) {
+            return null;
+        }
+        asyncRequest.update(new Date(), "Re-Enabling Controller Services", 60);
 
         // Step 13. Re-enable all disabled controller services
         final Pause enableServicesPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
         final Set<AffectedComponentEntity> servicesToEnable = getUpdatedEntities(enabledServices, user);
         logger.info("Successfully updated flow; re-enabling {} Controller Services", servicesToEnable.size());
         componentLifecycle.activateControllerServices(exampleUri, user, groupId, servicesToEnable, ControllerServiceState.ENABLED, enableServicesPause);
-        asyncRequest.setLastUpdated(new Date());
+
+        if (asyncRequest.isCancelled()) {
+            return null;
+        }
+        asyncRequest.update(new Date(), "Restarting Processors", 80);
 
         // Step 14. Restart all components
         final Set<AffectedComponentEntity> componentsToStart = getUpdatedEntities(runningComponents, user);
         final Pause startComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
         logger.info("Restarting {} Processors", componentsToStart.size());
         componentLifecycle.scheduleComponents(exampleUri, user, groupId, componentsToStart, ScheduledState.RUNNING, startComponentsPause);
-        asyncRequest.setLastUpdated(new Date());
+
+        if (asyncRequest.isCancelled()) {
+            return null;
+        }
+        asyncRequest.update(new Date(), "Complete", 100);
 
         return serviceFacade.getVersionControlInformation(groupId);
     }
@@ -1318,42 +1383,6 @@ public class VersionsResource extends ApplicationResource {
     }
 
 
-    private void authorizeAffectedComponents(final AuthorizableLookup lookup, final Set<AffectedComponentEntity> affectedComponents) {
-        final Map<String, List<AffectedComponentEntity>> componentsByType = affectedComponents.stream()
-            .collect(Collectors.groupingBy(entity -> entity.getComponent().getReferenceType()));
-
-        authorize(componentsByType.get(ComponentType.PROCESSOR.name()), id -> lookup.getProcessor(id).getAuthorizable());
-        authorize(componentsByType.get(ComponentType.CONTROLLER_SERVICE.name()), id -> lookup.getControllerService(id).getAuthorizable());
-
-        authorize(componentsByType.get(ComponentType.CONNECTION.name()), id -> lookup.getConnection(id).getAuthorizable());
-        authorize(componentsByType.get(ComponentType.FUNNEL.name()), id -> lookup.getFunnel(id));
-        authorize(componentsByType.get(ComponentType.INPUT_PORT.name()), id -> lookup.getInputPort(id));
-        authorize(componentsByType.get(ComponentType.OUTPUT_PORT.name()), id -> lookup.getOutputPort(id));
-        authorize(componentsByType.get(ComponentType.LABEL.name()), id -> lookup.getLabel(id));
-
-        authorize(componentsByType.get(ComponentType.PROCESS_GROUP.name()), id -> lookup.getProcessGroup(id).getAuthorizable());
-        authorize(componentsByType.get(ComponentType.REMOTE_PROCESS_GROUP.name()), id -> lookup.getRemoteProcessGroup(id));
-
-
-        // Remote Input Ports and Remote Output Ports are not authorized independently but rather at the Remote Process Group level,
-        // so we have to treat these a little differently.
-        componentsByType.getOrDefault(ComponentType.REMOTE_INPUT_PORT.name(), Collections.emptyList()).stream()
-            .forEach(affectedPort -> {
-                final String rpgId = affectedPort.getComponent().getProcessGroupId();
-                final Authorizable rpg = lookup.getRemoteProcessGroup(rpgId);
-                rpg.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-                rpg.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-            });
-
-        componentsByType.getOrDefault(ComponentType.REMOTE_OUTPUT_PORT.name(), Collections.emptyList()).stream()
-            .forEach(affectedPort -> {
-                final String rpgId = affectedPort.getComponent().getProcessGroupId();
-                final Authorizable rpg = lookup.getRemoteProcessGroup(rpgId);
-                rpg.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-                rpg.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-            });
-    }
-
     private Set<AffectedComponentEntity> getUpdatedEntities(final Set<AffectedComponentEntity> originalEntities, final NiFiUser user) {
         final Set<AffectedComponentEntity> entities = new LinkedHashSet<>();
 
@@ -1373,17 +1402,6 @@ public class VersionsResource extends ApplicationResource {
     }
 
 
-    private void authorize(final List<AffectedComponentEntity> componentDtos, final Function<String, Authorizable> authFunction) {
-        if (componentDtos != null) {
-            for (final AffectedComponentEntity entity : componentDtos) {
-                final Authorizable authorizable = authFunction.apply(entity.getComponent().getId());
-                authorizable.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-                authorizable.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-            }
-        }
-    }
-
-
     public void setServiceFacade(NiFiServiceFacade serviceFacade) {
         this.serviceFacade = serviceFacade;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
index 4b87b50..5dcb125 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
@@ -110,7 +110,6 @@ public class AsyncRequestManager<T> implements RequestManager<T> {
                 } catch (final Exception e) {
                     logger.error("Failed to perform asynchronous task", e);
                     request.setFailureReason("Encountered unexpected error when performing asynchronous task: " + e);
-                    request.setLastUpdated(new Date());
                 }
             }
         });

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
index 2c14008..1309eee 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
@@ -39,11 +39,23 @@ public interface AsynchronousWebRequest<T> {
     Date getLastUpdated();
 
     /**
-     * Updates the Date at which the status of this request was last updated
+     * @return the current state of the request
+     */
+    public String getState();
+
+    /**
+     * @return the current percent complete, between 0 and 100 (inclusive)
+     */
+    public int getPercentComplete();
+
+    /**
+     * Updates the request to indicate the new state and percent complete
      *
-     * @param date the date at which the status of this request was last updated
+     * @param date the last updated time
+     * @param state the new state
+     * @param percentComplete The percentage complete, between 0 and 100 (inclusive)
      */
-    void setLastUpdated(Date date);
+    void update(Date date, String state, int percentComplete);
 
     /**
      * @return the user who submitted the request
@@ -77,4 +89,14 @@ public interface AsynchronousWebRequest<T> {
      * @return the results of the request, if it completed successfully, or <code>null</code> if the request either has no completed or failed
      */
     T getResults();
+
+    /**
+     * Cancels the request so that no more steps can be completed
+     */
+    void cancel();
+
+    /**
+     * @return <code>true</code> if the request has been canceled, <code>false</code> otherwise
+     */
+    boolean isCancelled();
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
index 8ba9a58..4810a32 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/StandardAsynchronousWebRequest.java
@@ -29,13 +29,17 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
 
     private volatile boolean complete = false;
     private volatile Date lastUpdated = new Date();
+    private volatile String state;
+    private volatile int percentComplete;
     private volatile String failureReason;
+    private volatile boolean cancelled;
     private volatile T results;
 
-    public StandardAsynchronousWebRequest(final String requestId, final String processGroupId, final NiFiUser user) {
+    public StandardAsynchronousWebRequest(final String requestId, final String processGroupId, final NiFiUser user, final String state) {
         this.id = requestId;
         this.processGroupId = processGroupId;
         this.user = user;
+        this.state = state;
     }
 
     public String getRequestId() {
@@ -57,6 +61,8 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
         this.complete = true;
         this.results = results;
         this.lastUpdated = new Date();
+        this.percentComplete = 100;
+        this.state = "Complete";
     }
 
     @Override
@@ -65,8 +71,34 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
     }
 
     @Override
-    public void setLastUpdated(final Date date) {
-        this.lastUpdated = lastUpdated;
+    public String getState() {
+        return state;
+    }
+
+    @Override
+    public int getPercentComplete() {
+        return percentComplete;
+    }
+
+    @Override
+    public void update(Date date, String state, int percentComplete) {
+        if (percentComplete < 0 || percentComplete > 100) {
+            throw new IllegalArgumentException("Cannot set percent complete to a value of " + percentComplete + "; it must be between 0 and 100.");
+        }
+
+        if (isCancelled()) {
+            throw new IllegalStateException("Cannot update state because request has already been cancelled by user");
+        }
+
+        if (isComplete()) {
+            final String failure = getFailureReason();
+            final String explanation = failure == null ? "successfully" : "with failure reason: " + failure;
+            throw new IllegalStateException("Cannot update state to '" + state + "' because request is already completed " + explanation);
+        }
+
+        this.lastUpdated = date;
+        this.state = state;
+        this.percentComplete = percentComplete;
     }
 
     @Override
@@ -79,6 +111,7 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
         this.failureReason = Objects.requireNonNull(explanation);
         this.complete = true;
         this.results = null;
+        this.lastUpdated = new Date();
     }
 
     @Override
@@ -90,4 +123,17 @@ public class StandardAsynchronousWebRequest<T> implements AsynchronousWebRequest
     public T getResults() {
         return results;
     }
+
+    @Override
+    public void cancel() {
+        this.cancelled = true;
+        percentComplete = 100;
+        state = "Canceled by user";
+        setFailureReason("Request cancelled by user");
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return cancelled;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 3639b18..1c1e729 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -115,8 +115,12 @@ import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.diff.FlowComparison;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
+import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedFunnel;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedLabel;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedPort;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
@@ -2174,6 +2178,38 @@ public final class DtoFactory {
         return dto;
     }
 
+
+    public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison) {
+        final Map<ComponentDifferenceDTO, List<String>> differencesByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : comparison.getDifferences()) {
+            final ComponentDifferenceDTO componentDiff = createComponentDifference(difference);
+            final List<String> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>());
+            differences.add(difference.getDescription());
+        }
+
+        for (final Map.Entry<ComponentDifferenceDTO, List<String>> entry : differencesByComponent.entrySet()) {
+            entry.getKey().setDifferences(entry.getValue());
+        }
+
+        return differencesByComponent.keySet();
+    }
+
+    private ComponentDifferenceDTO createComponentDifference(final FlowDifference difference) {
+        VersionedComponent component = difference.getComponentA();
+        if (component == null) {
+            component = difference.getComponentB();
+        }
+
+        final ComponentDifferenceDTO dto = new ComponentDifferenceDTO();
+        dto.setComponentId(component.getIdentifier());
+        dto.setComponentName(component.getName());
+        dto.setComponentType(component.getComponentType().name());
+        dto.setProcessGroupId(dto.getProcessGroupId());
+        return dto;
+    }
+
+
     public VersionControlInformationDTO createVersionControlInformationDto(final ProcessGroup group) {
         if (group == null) {
             return null;
@@ -2187,10 +2223,12 @@ public final class DtoFactory {
         final VersionControlInformationDTO dto = new VersionControlInformationDTO();
         dto.setGroupId(group.getIdentifier());
         dto.setRegistryId(versionControlInfo.getRegistryIdentifier());
+        dto.setRegistryName(versionControlInfo.getRegistryName());
         dto.setBucketId(versionControlInfo.getBucketIdentifier());
+        dto.setBucketName(versionControlInfo.getBucketName());
         dto.setFlowId(versionControlInfo.getFlowIdentifier());
-        // TODO - need to get flow name here
-        dto.setFlowName(group.getName());
+        dto.setFlowName(versionControlInfo.getFlowName());
+        dto.setFlowDescription(versionControlInfo.getFlowDescription());
         dto.setVersion(versionControlInfo.getVersion());
         dto.setCurrent(versionControlInfo.getCurrent().orElse(null));
         dto.setModified(versionControlInfo.getModified().orElse(null));
@@ -2204,6 +2242,9 @@ public final class DtoFactory {
         group.getProcessors().stream()
             .map(proc -> (InstantiatedVersionedProcessor) proc)
             .forEach(proc -> mapping.put(proc.getInstanceId(), proc.getIdentifier()));
+        group.getFunnels().stream()
+            .map(funnel -> (InstantiatedVersionedFunnel) funnel)
+            .forEach(funnel -> mapping.put(funnel.getInstanceId(), funnel.getIdentifier()));
         group.getInputPorts().stream()
             .map(port -> (InstantiatedVersionedPort) port)
             .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
@@ -2224,13 +2265,17 @@ public final class DtoFactory {
             .forEach(rpg -> {
                 mapping.put(rpg.getInstanceId(), rpg.getIdentifier());
 
-                rpg.getInputPorts().stream()
-                    .map(port -> (InstantiatedVersionedRemoteGroupPort) port)
-                    .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+                if (rpg.getInputPorts() != null) {
+                    rpg.getInputPorts().stream()
+                        .map(port -> (InstantiatedVersionedRemoteGroupPort) port)
+                        .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+                }
 
-                rpg.getOutputPorts().stream()
-                    .map(port -> (InstantiatedVersionedRemoteGroupPort) port)
-                    .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+                if (rpg.getOutputPorts() != null) {
+                    rpg.getOutputPorts().stream()
+                        .map(port -> (InstantiatedVersionedRemoteGroupPort) port)
+                        .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier()));
+                }
             });
 
         group.getProcessGroups().stream()
@@ -3407,9 +3452,12 @@ public final class DtoFactory {
 
         final VersionControlInformationDTO copy = new VersionControlInformationDTO();
         copy.setRegistryId(original.getRegistryId());
+        copy.setRegistryName(original.getRegistryName());
         copy.setBucketId(original.getBucketId());
+        copy.setBucketName(original.getBucketName());
         copy.setFlowId(original.getFlowId());
         copy.setFlowName(original.getFlowName());
+        copy.setFlowDescription(original.getFlowDescription());
         copy.setVersion(original.getVersion());
         copy.setCurrent(original.getCurrent());
         copy.setModified(original.getModified());

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
index 19f2de4..4f5af74 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
@@ -17,8 +17,14 @@
 
 package org.apache.nifi.web.dao.impl;
 
+import java.io.IOException;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionedFlow;
@@ -71,37 +77,48 @@ public class FlowRegistryDAO implements RegistryDAO {
                 throw new IllegalArgumentException("The specified registry id is unknown to this NiFi.");
             }
 
-            return flowRegistry.getBuckets(user);
-        } catch (final IOException ioe) {
-            throw new NiFiCoreException("Unable to obtain bucket listing: " + ioe.getMessage(), ioe);
+            final Set<Bucket> buckets = flowRegistry.getBuckets(user);
+            final Set<Bucket> sortedBuckets = new TreeSet<>((b1, b2) -> b1.getName().compareTo(b2.getName()));
+            sortedBuckets.addAll(buckets);
+            return sortedBuckets;
+        } catch (final IOException | NiFiRegistryException ioe) {
+            throw new NiFiCoreException("Unable to obtain listing of buckets: " + ioe, ioe);
         }
     }
 
 
     @Override
     public Set<VersionedFlow> getFlowsForUser(String registryId, String bucketId, NiFiUser user) {
-        final Set<Bucket> bucketsForUser = getBucketsForUser(registryId, user);
+        try {
+            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
+            if (flowRegistry == null) {
+                throw new IllegalArgumentException("The specified registry id is unknown to this NiFi.");
+            }
 
-        // TODO - implement getBucket(bucketId, user)
-        final Bucket bucket = bucketsForUser.stream().filter(b -> b.getIdentifier().equals(bucketId)).findFirst().orElse(null);
-        if (bucket == null) {
-            throw new IllegalArgumentException("The specified bucket is not available.");
+            final Set<VersionedFlow> flows = flowRegistry.getFlows(bucketId, user);
+            final Set<VersionedFlow> sortedFlows = new TreeSet<>((f1, f2) -> f1.getName().compareTo(f2.getName()));
+            sortedFlows.addAll(flows);
+            return sortedFlows;
+        } catch (final IOException | NiFiRegistryException ioe) {
+            throw new NiFiCoreException("Unable to obtain listing of flows for bucket with ID " + bucketId + ": " + ioe, ioe);
         }
-
-        return bucket.getVersionedFlows();
     }
 
     @Override
     public Set<VersionedFlowSnapshotMetadata> getFlowVersionsForUser(String registryId, String bucketId, String flowId, NiFiUser user) {
-        final Set<VersionedFlow> flowsForUser = getFlowsForUser(registryId, bucketId, user);
+        try {
+            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
+            if (flowRegistry == null) {
+                throw new IllegalArgumentException("The specified registry id is unknown to this NiFi.");
+            }
 
-        // TODO - implement getFlow(bucketId, flowId, user)
-        final VersionedFlow versionedFlow = flowsForUser.stream().filter(vf -> vf.getIdentifier().equals(flowId)).findFirst().orElse(null);
-        if (versionedFlow == null) {
-            throw new IllegalArgumentException("The specified flow is not available.");
+            final Set<VersionedFlowSnapshotMetadata> flowVersions = flowRegistry.getFlowVersions(bucketId, flowId, user);
+            final Set<VersionedFlowSnapshotMetadata> sortedFlowVersions = new TreeSet<>((f1, f2) -> Integer.compare(f1.getVersion(), f2.getVersion()));
+            sortedFlowVersions.addAll(flowVersions);
+            return sortedFlowVersions;
+        } catch (final IOException | NiFiRegistryException ioe) {
+            throw new NiFiCoreException("Unable to obtain listing of versions for bucket with ID " + bucketId + " and flow with ID " + flowId + ": " + ioe, ioe);
         }
-
-        return versionedFlow.getSnapshotMetadata();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
index 35c537d..f830e9b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardInputPortDAO.java
@@ -38,7 +38,11 @@ public class StandardInputPortDAO extends ComponentDAO implements PortDAO {
 
     private Port locatePort(final String portId) {
         final ProcessGroup rootGroup = flowController.getGroup(flowController.getRootGroupId());
-        final Port port = rootGroup.findInputPort(portId);
+        Port port = rootGroup.findInputPort(portId);
+
+        if (port == null) {
+            port = rootGroup.findOutputPort(portId);
+        }
 
         if (port == null) {
             throw new ResourceNotFoundException(String.format("Unable to find port with id '%s'.", portId));
@@ -50,7 +54,7 @@ public class StandardInputPortDAO extends ComponentDAO implements PortDAO {
     @Override
     public boolean hasPort(String portId) {
         final ProcessGroup rootGroup = flowController.getGroup(flowController.getRootGroupId());
-        return rootGroup.findInputPort(portId) != null;
+        return rootGroup.findInputPort(portId) != null || rootGroup.findOutputPort(portId) != null;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 963220e..78f3e31 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -25,6 +25,7 @@ import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
@@ -238,11 +239,15 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
 
         final String registryId = versionControlInformation.getRegistryId();
-        final String bucketId = versionControlInformation.getBucketId();
-        final String flowId = versionControlInformation.getFlowId();
-        final int version = versionControlInformation.getVersion();
+        final FlowRegistry flowRegistry = flowController.getFlowRegistryClient().getFlowRegistry(registryId);
+        final String registryName = flowRegistry == null ? registryId : flowRegistry.getName();
+
+        final StandardVersionControlInformation vci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
+            .registryName(registryName)
+            .modified(false)
+            .current(true)
+            .build();
 
-        final VersionControlInformation vci = new StandardVersionControlInformation(registryId, bucketId, flowId, version, null, false, true);
         group.setVersionControlInformation(vci, versionedComponentMapping);
 
         return group;
@@ -261,14 +266,9 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
         group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified, updateSettings);
 
-        final StandardVersionControlInformation svci = new StandardVersionControlInformation(
-            versionControlInformation.getRegistryId(),
-            versionControlInformation.getBucketId(),
-            versionControlInformation.getFlowId(),
-            versionControlInformation.getVersion(),
-            proposedSnapshot.getFlowContents(),
-            versionControlInformation.getModified(),
-            versionControlInformation.getCurrent());
+        final StandardVersionControlInformation svci = StandardVersionControlInformation.Builder.fromDto(versionControlInformation)
+            .flowSnapshot(proposedSnapshot.getFlowContents())
+            .build();
 
         group.setVersionControlInformation(svci, Collections.emptyMap());
         return group;

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
index a6efb71..1f83a6f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/CancellableTimedPause.java
@@ -24,9 +24,10 @@ public class CancellableTimedPause implements Pause {
     private final long pauseNanos;
     private volatile boolean cancelled = false;
 
-    public CancellableTimedPause(final long pauseTime, final long expirationTime, final TimeUnit timeUnit) {
-        final long expirationNanos = TimeUnit.NANOSECONDS.convert(expirationTime, timeUnit);
-        expirationNanoTime = System.nanoTime() + expirationNanos;
+    public CancellableTimedPause(final long pauseTime, final long expirationPeriod, final TimeUnit timeUnit) {
+        final long expirationNanos = TimeUnit.NANOSECONDS.convert(expirationPeriod, timeUnit);
+        final long expirationTime = System.nanoTime() + expirationNanos;
+        expirationNanoTime = expirationTime < 0 ? Long.MAX_VALUE : expirationTime;
         pauseNanos = Math.max(1L, TimeUnit.NANOSECONDS.convert(pauseTime, timeUnit));
     }
 
@@ -44,7 +45,7 @@ public class CancellableTimedPause implements Pause {
         final long maxWaitTime = System.nanoTime() + pauseNanos;
         while (sysTime < maxWaitTime) {
             try {
-                TimeUnit.NANOSECONDS.wait(pauseNanos);
+                TimeUnit.NANOSECONDS.sleep(pauseNanos);
             } catch (final InterruptedException ie) {
                 Thread.currentThread().interrupt();
                 return false;

http://git-wip-us.apache.org/repos/asf/nifi/blob/6b00dff1/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 490862c..584cac6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1641,6 +1641,12 @@
                 <version>${nifi.registry.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.apache.nifi.registry</groupId>
+                <artifactId>nifi-registry-client</artifactId>
+                <version>${nifi.registry.version}</version>
+            </dependency>
+            
+            <dependency>
                 <groupId>com.jayway.jsonpath</groupId>
                 <artifactId>json-path</artifactId>
                 <version>2.0.0</version>


[29/50] nifi git commit: NIFI-4436: - Addressing miscellaneous minor UX issues. - Updating comments UX for all components. - Updating the styling of PG and RPG to be more consistent. - Adding the icons for nested versioned process groups. - Calculating t

Posted by bb...@apache.org.
NIFI-4436:
- Addressing miscellaneous minor UX issues.
- Updating comments UX for all components.
- Updating the styling of PG and RPG to be more consistent.
- Adding the icons for nested versioned process groups.
- Calculating the number/states of nested versioned process groups.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/d34fb5e2
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/d34fb5e2
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/d34fb5e2

Branch: refs/heads/master
Commit: d34fb5e2ef967b0704621ffa0115283c3ccdc070
Parents: e160670
Author: Matt Gilman <ma...@gmail.com>
Authored: Tue Nov 28 16:22:11 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:54 2018 -0500

----------------------------------------------------------------------
 .../nifi/web/api/dto/ProcessGroupDTO.java       |  59 ++++-
 .../apache/nifi/groups/ProcessGroupCounts.java  |  32 ++-
 .../apache/nifi/groups/RemoteProcessGroup.java  |   2 +-
 .../nifi/groups/RemoteProcessGroupCounts.java   |  36 +++
 .../apache/nifi/controller/TemplateUtils.java   |   5 +
 .../nifi/groups/StandardProcessGroup.java       |  81 ++++--
 .../nifi/remote/StandardRemoteProcessGroup.java | 103 +++-----
 .../nifi/web/StandardNiFiServiceFacade.java     |  10 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  14 +-
 .../canvas/revert-local-changes-dialog.jsp      |   6 +-
 .../canvas/show-local-changes-dialog.jsp        |   6 +-
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |   8 +-
 .../nifi-web-ui/src/main/webapp/css/graph.css   |   6 +-
 .../nifi-web-ui/src/main/webapp/css/main.css    |  32 +++
 .../main/webapp/js/nf/canvas/nf-canvas-utils.js |   4 +
 .../main/webapp/js/nf/canvas/nf-flow-version.js |  65 +++--
 .../src/main/webapp/js/nf/canvas/nf-port.js     |  46 ++++
 .../webapp/js/nf/canvas/nf-process-group.js     | 252 ++++++++++++++++---
 .../main/webapp/js/nf/canvas/nf-processor.js    |  46 ++++
 .../js/nf/canvas/nf-remote-process-group.js     |  89 ++++---
 20 files changed, 687 insertions(+), 215 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
index 7faf10b..d283510 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ProcessGroupDTO.java
@@ -30,7 +30,7 @@ public class ProcessGroupDTO extends ComponentDTO {
     private String name;
     private String comments;
     private Map<String, String> variables;
-    private VersionControlInformationDTO versionControlInfo;
+    private VersionControlInformationDTO versionControlInformation;
 
     private Integer runningCount;
     private Integer stoppedCount;
@@ -39,6 +39,12 @@ public class ProcessGroupDTO extends ComponentDTO {
     private Integer activeRemotePortCount;
     private Integer inactiveRemotePortCount;
 
+    private Integer upToDateCount;
+    private Integer locallyModifiedCount;
+    private Integer staleCount;
+    private Integer locallyModifiedAndStaleCount;
+    private Integer syncFailureCount;
+
     private Integer inputPortCount;
     private Integer outputPortCount;
 
@@ -204,6 +210,51 @@ public class ProcessGroupDTO extends ComponentDTO {
         this.inactiveRemotePortCount = inactiveRemotePortCount;
     }
 
+    @ApiModelProperty("The number of up to date versioned process groups in the process group.")
+    public Integer getUpToDateCount() {
+        return upToDateCount;
+    }
+
+    public void setUpToDateCount(Integer upToDateCount) {
+        this.upToDateCount = upToDateCount;
+    }
+
+    @ApiModelProperty("The number of locally modified versioned process groups in the process group.")
+    public Integer getLocallyModifiedCount() {
+        return locallyModifiedCount;
+    }
+
+    public void setLocallyModifiedCount(Integer locallyModifiedCount) {
+        this.locallyModifiedCount = locallyModifiedCount;
+    }
+
+    @ApiModelProperty("The number of stale versioned process groups in the process group.")
+    public Integer getStaleCount() {
+        return staleCount;
+    }
+
+    public void setStaleCount(Integer staleCount) {
+        this.staleCount = staleCount;
+    }
+
+    @ApiModelProperty("The number of locally modified and stale versioned process groups in the process group.")
+    public Integer getLocallyModifiedAndStaleCount() {
+        return locallyModifiedAndStaleCount;
+    }
+
+    public void setLocallyModifiedAndStaleCount(Integer locallyModifiedAndStaleCount) {
+        this.locallyModifiedAndStaleCount = locallyModifiedAndStaleCount;
+    }
+
+    @ApiModelProperty("The number of versioned process groups in the process group that are unable to sync to a registry.")
+    public Integer getSyncFailureCount() {
+        return syncFailureCount;
+    }
+
+    public void setSyncFailureCount(Integer syncFailureCount) {
+        this.syncFailureCount = syncFailureCount;
+    }
+
     @ApiModelProperty(value = "The variables that are configured for the Process Group. Note that this map contains only "
         + "those variables that are defined on this Process Group and not any variables that are defined in the parent "
         + "Process Group, etc. I.e., this Map will not contain all variables that are accessible by components in this "
@@ -219,10 +270,10 @@ public class ProcessGroupDTO extends ComponentDTO {
     @ApiModelProperty("The Version Control information that indicates which Flow Registry, and where in the Flow Registry, "
         + "this Process Group is tracking to; or null if this Process Group is not under version control")
     public VersionControlInformationDTO getVersionControlInformation() {
-        return versionControlInfo;
+        return versionControlInformation;
     }
 
-    public void setVersionControlInformation(final VersionControlInformationDTO versionControlInfo) {
-        this.versionControlInfo = versionControlInfo;
+    public void setVersionControlInformation(final VersionControlInformationDTO versionControlInformation) {
+        this.versionControlInformation = versionControlInformation;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroupCounts.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroupCounts.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroupCounts.java
index 3eb594b..394e5d3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroupCounts.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroupCounts.java
@@ -18,10 +18,11 @@ package org.apache.nifi.groups;
 
 public class ProcessGroupCounts {
 
-    private final int inputPortCount, outputPortCount, runningCount, stoppedCount, invalidCount, disabledCount, activeRemotePortCount, inactiveRemotePortCount;
+    private final int inputPortCount, outputPortCount, runningCount, stoppedCount, invalidCount, disabledCount, activeRemotePortCount, inactiveRemotePortCount,
+            upToDateCount, locallyModifiedCount, staleCount, locallyModifiedAndStaleCount, syncFailureCount;
 
-    public ProcessGroupCounts(final int inputPortCount, final int outputPortCount, final int runningCount,
-            final int stoppedCount, final int invalidCount, final int disabledCount, final int activeRemotePortCount, final int inactiveRemotePortCount) {
+    public ProcessGroupCounts(int inputPortCount, int outputPortCount, int runningCount, int stoppedCount, int invalidCount, int disabledCount, int activeRemotePortCount,
+                              int inactiveRemotePortCount, int upToDateCount, int locallyModifiedCount, int staleCount, int locallyModifiedAndStaleCount, int syncFailureCount) {
         this.inputPortCount = inputPortCount;
         this.outputPortCount = outputPortCount;
         this.runningCount = runningCount;
@@ -30,6 +31,11 @@ public class ProcessGroupCounts {
         this.disabledCount = disabledCount;
         this.activeRemotePortCount = activeRemotePortCount;
         this.inactiveRemotePortCount = inactiveRemotePortCount;
+        this.upToDateCount = upToDateCount;
+        this.locallyModifiedCount = locallyModifiedCount;
+        this.staleCount = staleCount;
+        this.locallyModifiedAndStaleCount = locallyModifiedAndStaleCount;
+        this.syncFailureCount = syncFailureCount;
     }
 
     public int getInputPortCount() {
@@ -63,4 +69,24 @@ public class ProcessGroupCounts {
     public int getInactiveRemotePortCount() {
         return inactiveRemotePortCount;
     }
+
+    public int getUpToDateCount() {
+        return upToDateCount;
+    }
+
+    public int getLocallyModifiedCount() {
+        return locallyModifiedCount;
+    }
+
+    public int getStaleCount() {
+        return staleCount;
+    }
+
+    public int getLocallyModifiedAndStaleCount() {
+        return locallyModifiedAndStaleCount;
+    }
+
+    public int getSyncFailureCount() {
+        return syncFailureCount;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
index e4da31b..0dd6070 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroup.java
@@ -73,7 +73,7 @@ public interface RemoteProcessGroup extends ComponentAuthorizable, Positionable,
 
     RemoteGroupPort getOutputPort(String id);
 
-    ProcessGroupCounts getCounts();
+    RemoteProcessGroupCounts getCounts();
 
     void refreshFlowContents() throws CommunicationsException;
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupCounts.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupCounts.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupCounts.java
new file mode 100644
index 0000000..e3163f6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/RemoteProcessGroupCounts.java
@@ -0,0 +1,36 @@
+/*
+ * 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.groups;
+
+public class RemoteProcessGroupCounts {
+
+    private final int inputPortCount, outputPortCount;
+
+    public RemoteProcessGroupCounts(int inputPortCount, int outputPortCount) {
+        this.inputPortCount = inputPortCount;
+        this.outputPortCount = outputPortCount;
+    }
+
+    public int getInputPortCount() {
+        return inputPortCount;
+    }
+
+    public int getOutputPortCount() {
+        return outputPortCount;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/TemplateUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/TemplateUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/TemplateUtils.java
index 9583bbb..6bfa49d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/TemplateUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/TemplateUtils.java
@@ -127,6 +127,11 @@ public class TemplateUtils {
             processGroupDTO.setOutputPortCount(null);
             processGroupDTO.setRunningCount(null);
             processGroupDTO.setStoppedCount(null);
+            processGroupDTO.setUpToDateCount(null);
+            processGroupDTO.setLocallyModifiedCount(null);
+            processGroupDTO.setStaleCount(null);
+            processGroupDTO.setLocallyModifiedAndStaleCount(null);
+            processGroupDTO.setSyncFailureCount(null);
 
             scrubSnippet(processGroupDTO.getContents());
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 77be3fe..9a14464 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -16,31 +16,6 @@
  */
 package org.apache.nifi.groups;
 
-import static java.util.Objects.requireNonNull;
-
-import java.io.IOException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -139,6 +114,31 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+
 public final class StandardProcessGroup implements ProcessGroup {
 
     private final String id;
@@ -278,6 +278,12 @@ public final class StandardProcessGroup implements ProcessGroup {
         int activeRemotePorts = 0;
         int inactiveRemotePorts = 0;
 
+        int upToDate = 0;
+        int locallyModified = 0;
+        int stale = 0;
+        int locallyModifiedAndStale = 0;
+        int syncFailure = 0;
+
         readLock.lock();
         try {
             for (final ProcessorNode procNode : processors.values()) {
@@ -324,6 +330,27 @@ public final class StandardProcessGroup implements ProcessGroup {
                 stopped += childCounts.getStoppedCount();
                 invalid += childCounts.getInvalidCount();
                 disabled += childCounts.getDisabledCount();
+
+                // update the vci counts for this child group
+                final VersionControlInformation vci = childGroup.getVersionControlInformation();
+                if (vci != null) {
+                    if (vci.isModified() && !vci.isCurrent()) {
+                        locallyModifiedAndStale += 1;
+                    } else if (!vci.isCurrent()) {
+                        stale += 1;
+                    } else if (vci.isModified()) {
+                        locallyModified += 1;
+                    } else {
+                        upToDate += 1;
+                    }
+                }
+
+                // update the vci counts for all nested groups within the child
+                upToDate += childCounts.getUpToDateCount();
+                locallyModified += childCounts.getLocallyModifiedCount();
+                stale += childCounts.getStaleCount();
+                locallyModifiedAndStale += childCounts.getLocallyModifiedAndStaleCount();
+                syncFailure += childCounts.getSyncFailureCount();
             }
 
             for (final RemoteProcessGroup remoteGroup : findAllRemoteProcessGroups()) {
@@ -358,8 +385,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             readLock.unlock();
         }
 
-        return new ProcessGroupCounts(inputPortCount, outputPortCount, running, stopped,
-                invalid, disabled, activeRemotePorts, inactiveRemotePorts);
+        return new ProcessGroupCounts(inputPortCount, outputPortCount, running, stopped, invalid, disabled, activeRemotePorts,
+                inactiveRemotePorts, upToDate, locallyModified, stale, locallyModifiedAndStale, syncFailure);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index 039ac66..67710fe 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -16,40 +16,6 @@
  */
 package org.apache.nifi.remote;
 
-import static java.util.Objects.requireNonNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import javax.net.ssl.SSLContext;
-import javax.ws.rs.core.Response;
-
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.Resource;
 import org.apache.nifi.authorization.resource.Authorizable;
@@ -70,6 +36,7 @@ import org.apache.nifi.events.EventReporter;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.ProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor;
 import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
 import org.apache.nifi.remote.protocol.http.HttpProxy;
@@ -84,6 +51,39 @@ import org.apache.nifi.web.api.dto.PortDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.core.Response;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+
 /**
  * Represents the Root Process Group of a remote NiFi Instance. Holds
  * information about that remote instance, as well as {@link IncomingPort}s and
@@ -137,7 +137,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
     // Maps a Port Name to a PullingPort that can be used to receive files from that port
     private final Map<String, StandardRemoteGroupPort> outputPorts = new HashMap<>();
 
-    private ProcessGroupCounts counts = new ProcessGroupCounts(0, 0, 0, 0, 0, 0, 0, 0);
+    private RemoteProcessGroupCounts counts = new RemoteProcessGroupCounts(0, 0);
     private Long refreshContentsTimestamp = null;
     private Boolean destinationSecure;
     private Integer listeningPort;
@@ -829,7 +829,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
     }
 
     @Override
-    public ProcessGroupCounts getCounts() {
+    public RemoteProcessGroupCounts getCounts() {
         readLock.lock();
         try {
             return counts;
@@ -838,7 +838,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
         }
     }
 
-    private void setCounts(final ProcessGroupCounts counts) {
+    private void setCounts(final RemoteProcessGroupCounts counts) {
         writeLock.lock();
         try {
             this.counts = counts;
@@ -910,39 +910,12 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
                 if (dto.getOutputPortCount() != null) {
                     outputPortCount = dto.getOutputPortCount();
                 }
-                int runningCount = 0;
-                if (dto.getRunningCount() != null) {
-                    runningCount = dto.getRunningCount();
-                }
-                int stoppedCount = 0;
-                if (dto.getStoppedCount() != null) {
-                    stoppedCount = dto.getStoppedCount();
-                }
-                int invalidCount = 0;
-                if (dto.getInvalidCount() != null) {
-                    invalidCount = dto.getInvalidCount();
-                }
-                int disabledCount = 0;
-                if (dto.getDisabledCount() != null) {
-                    disabledCount = dto.getDisabledCount();
-                }
-
-                int activeRemotePortCount = 0;
-                if (dto.getActiveRemotePortCount() != null) {
-                    activeRemotePortCount = dto.getActiveRemotePortCount();
-                }
-
-                int inactiveRemotePortCount = 0;
-                if (dto.getInactiveRemotePortCount() != null) {
-                    inactiveRemotePortCount = dto.getInactiveRemotePortCount();
-                }
 
                 this.listeningPort = dto.getRemoteSiteListeningPort();
                 this.listeningHttpPort = dto.getRemoteSiteHttpListeningPort();
                 this.destinationSecure = dto.isSiteToSiteSecure();
 
-                final ProcessGroupCounts newCounts = new ProcessGroupCounts(inputPortCount, outputPortCount,
-                        runningCount, stoppedCount, invalidCount, disabledCount, activeRemotePortCount, inactiveRemotePortCount);
+                final RemoteProcessGroupCounts newCounts = new RemoteProcessGroupCounts(inputPortCount, outputPortCount);
                 setCounts(newCounts);
                 this.refreshContentsTimestamp = System.currentTimeMillis();
             } finally {

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index e0594fa..98c7bc8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,9 +16,7 @@
  */
 package org.apache.nifi.web;
 
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
-
+import com.google.common.collect.Sets;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
@@ -269,8 +267,8 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Sets;
-
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -3018,6 +3016,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final ControllerDTO controllerDTO = new ControllerDTO();
         controllerDTO.setId(controllerFacade.getRootGroupId());
         controllerDTO.setInstanceId(controllerFacade.getInstanceId());
+        controllerDTO.setName(controllerFacade.getName());
+        controllerDTO.setComments(controllerFacade.getComments());
         controllerDTO.setInputPorts(inputPortDtos);
         controllerDTO.setOutputPorts(outputPortDtos);
         controllerDTO.setInputPortCount(inputPortDtos.size());

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index fb60604..5b33d90 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -101,6 +101,7 @@ import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.ProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroupCounts;
 import org.apache.nifi.history.History;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarClassLoaders;
@@ -1648,7 +1649,7 @@ public final class DtoFactory {
         dto.setInactiveRemoteOutputPortCount(inactiveRemoteOutputPortCount);
         dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null));
 
-        final ProcessGroupCounts counts = group.getCounts();
+        final RemoteProcessGroupCounts counts = group.getCounts();
         if (counts != null) {
             dto.setInputPortCount(counts.getInputPortCount());
             dto.setOutputPortCount(counts.getOutputPortCount());
@@ -2175,6 +2176,11 @@ public final class DtoFactory {
         dto.setOutputPortCount(counts.getOutputPortCount());
         dto.setActiveRemotePortCount(counts.getActiveRemotePortCount());
         dto.setInactiveRemotePortCount(counts.getInactiveRemotePortCount());
+        dto.setUpToDateCount(counts.getUpToDateCount());
+        dto.setLocallyModifiedCount(counts.getLocallyModifiedCount());
+        dto.setStaleCount(counts.getStaleCount());
+        dto.setLocallyModifiedAndStaleCount(counts.getLocallyModifiedAndStaleCount());
+        dto.setSyncFailureCount(counts.getSyncFailureCount());
 
         return dto;
     }
@@ -3453,6 +3459,12 @@ public final class DtoFactory {
         copy.setActiveRemotePortCount(original.getActiveRemotePortCount());
         copy.setInactiveRemotePortCount(original.getInactiveRemotePortCount());
 
+        copy.setUpToDateCount(original.getUpToDateCount());
+        copy.setLocallyModifiedCount(original.getLocallyModifiedCount());
+        copy.setStaleCount(original.getStaleCount());
+        copy.setLocallyModifiedAndStaleCount(original.getLocallyModifiedAndStaleCount());
+        copy.setSyncFailureCount(original.getSyncFailureCount());
+
         if (original.getVariables() != null) {
             copy.setVariables(new HashMap<>(original.getVariables()));
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
index 45d4c4c..ce18052 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp
@@ -17,10 +17,8 @@
 <%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
 <div id="revert-local-changes-dialog" layout="column" class="hidden large-dialog">
     <div class="dialog-content">
-        <div class="setting">
-            <div class="setting-field">
-                Are you sure you want to revert changes? All flow configuration changes detailed below will be reverted to the last version.
-            </div>
+        <div class="setting local-changes-message">
+            <span id="revert-local-changes-message"></span>&nbsp;<span style="font-weight: bold;">Revert will remove all changes.</span>
         </div>
         <span id="revert-local-changes-process-group-id" class="hidden"></span>
         <div id="revert-local-changes-filter-controls">

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
index 9b122ae..bdd4d41 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/show-local-changes-dialog.jsp
@@ -17,10 +17,8 @@
 <%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
 <div id="show-local-changes-dialog" layout="column" class="hidden large-dialog">
     <div class="dialog-content">
-        <div class="setting">
-            <div class="setting-field">
-                The following changes have been made to the flow since the last version.
-            </div>
+        <div class="setting local-changes-message">
+            <span id="show-local-changes-message"></span>
         </div>
         <div id="show-local-changes-filter-controls">
             <div id="show-local-changes-filter-status" class="filter-status">

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index 69d4bea..9acbdde 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -269,17 +269,21 @@ div.progress-label {
 
 #revert-local-changes-table, #show-local-changes-table {
     position: absolute;
-    top: 80px;
+    top: 78px;
     left: 0px;
     right: 0px;
     bottom: 0px;
-    height: 225px;
+    height: 350px;
 }
 
 #revert-local-changes-filter, #show-local-changes-filter {
     width: 173px;
 }
 
+div.local-changes-message {
+    font-size: 13px;
+}
+
 /*
     Variable Registry
  */

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
index 204b1d1..4c77066 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
@@ -136,6 +136,11 @@ text.active-thread-count {
     font-weight: bold;
 }
 
+path.component-comments {
+    fill: #000;
+    stroke: #000;
+}
+
 /*
     Selection
 */
@@ -471,5 +476,4 @@ text.remote-process-group-transmission-secure {
 
 text.remote-process-group-last-refresh {
     fill: #728e9b;
-    text-anchor: end;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
index 1554356..d10761a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/main.css
@@ -153,6 +153,38 @@ div.context-menu-provenance {
     text-shadow: 0 0 4px rgba(255,255,255,1);
 }
 
+.up-to-date {
+    float: left;
+    color: #3da67a !important;
+    fill: #3da67a !important;
+    margin-top: 0px !important;
+    text-shadow: 0 0 4px rgba(255,255,255,1);
+}
+
+.locally-modified {
+    float: left;
+    color: #747474 !important;
+    fill: #747474 !important;
+    margin-top: 0px !important;
+    text-shadow: 0 0 4px rgba(255,255,255,1);
+}
+
+.stale {
+    float: left;
+    color: #c7685d !important;
+    fill: #c7685d !important;
+    margin-top: 0px !important;
+    text-shadow: 0 0 4px rgba(255,255,255,1);
+}
+
+.locally-modified-and-stale {
+    float: left;
+    color: #c7685d !important;
+    fill: #c7685d !important;
+    margin-top: 0px !important;
+    text-shadow: 0 0 4px rgba(255,255,255,1);
+}
+
 div.valid {
     float: left;
     background-color: transparent;

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
index 1aefa44..162da36 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
@@ -378,6 +378,10 @@
                 });
 
                 return refreshGraph;
+            } else {
+                return $.Deferred(function (deferred) {
+                    deferred.reject();
+                }).promise();
             }
         },
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index cb42c30..03aed70 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -126,13 +126,16 @@
 
             var localChangesData = localChangesGrid.getData();
             localChangesData.setItems([]);
+            localChangesData.setFilterArgs({
+                searchString: ''
+            });
         }
 
         filterInput.val('');
 
         displayedLabel.text('0');
         totalLabel.text('0');
-    }
+    };
 
     /**
      * Clears the version grid
@@ -386,9 +389,18 @@
     var sort = function (sortDetails, data) {
         // defines a function for sorting
         var comparer = function (a, b) {
-            var aString = nfCommon.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : '';
-            var bString = nfCommon.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : '';
-            return aString === bString ? 0 : aString > bString ? 1 : -1;
+            var aIsBlank = nfCommon.isBlank(a[sortDetails.columnId]);
+            var bIsBlank = nfCommon.isBlank(b[sortDetails.columnId]);
+
+            if (aIsBlank && bIsBlank) {
+                return 0;
+            } else if (aIsBlank) {
+                return 1;
+            } else if (bIsBlank) {
+                return -1;
+            }
+
+            return a[sortDetails.columnId] === b[sortDetails.columnId] ? 0 : a[sortDetails.columnId] > b[sortDetails.columnId] ? 1 : -1;
         };
 
         // perform the sort
@@ -584,7 +596,7 @@
             },
             {
                 id: 'differenceType',
-                name: 'Type',
+                name: 'Change Type',
                 field: 'differenceType',
                 formatter: valueFormatter,
                 sortable: true,
@@ -596,8 +608,7 @@
                 field: 'difference',
                 formatter: valueFormatter,
                 sortable: true,
-                resizable: true,
-                minWidth: 300
+                resizable: true
             },
             {
                 id: 'actions',
@@ -614,22 +625,21 @@
             inlineFilters: false
         });
         localChangesData.setFilterArgs({
-            searchString: '',
-            property: 'componentName'
+            searchString: getFilterText()
         });
         localChangesData.setFilter(filter);
 
         // initialize the sort
         sort({
-            columnId: 'version',
-            sortAsc: false
+            columnId: 'componentName',
+            sortAsc: true
         }, localChangesData);
 
         // initialize the grid
         var localChangesGrid = new Slick.Grid(localChangesTable, localChangesData, localChangesColumns, gridOptions);
         localChangesGrid.setSelectionModel(new Slick.RowSelectionModel());
         localChangesGrid.registerPlugin(new Slick.AutoTooltips());
-        localChangesGrid.setSortColumn('version', false);
+        localChangesGrid.setSortColumn('componentName', true);
         localChangesGrid.onSort.subscribe(function (e, args) {
             sort({
                 columnId: args.sortCol.id,
@@ -650,9 +660,13 @@
                     if (componentDifference.componentType === 'Controller Service') {
                         nfProcessGroupConfiguration.showConfiguration(componentDifference.processGroupId).done(function () {
                             nfProcessGroupConfiguration.selectControllerService(componentDifference.componentId);
+
+                            localChangesTable.closest('.large-dialog').modal('hide');
                         });
                     } else {
-                        nfCanvasUtils.showComponent(componentDifference.processGroupId, componentDifference.componentId);
+                        nfCanvasUtils.showComponent(componentDifference.processGroupId, componentDifference.componentId).done(function () {
+                            localChangesTable.closest('.large-dialog').modal('hide');
+                        });
                     }
                 }
             }
@@ -1240,10 +1254,11 @@
      * Shows local changes for the specified process group.
      *
      * @param processGroupId
+     * @param localChangesMessage
      * @param localChangesTable
      * @param totalLabel
      */
-    var loadLocalChanges = function (processGroupId, localChangesTable, totalLabel) {
+    var loadLocalChanges = function (processGroupId, localChangesMessage, localChangesTable, totalLabel) {
         var localChangesGrid = localChangesTable.data('gridInstance');
         var localChangesData = localChangesGrid.getData();
 
@@ -1255,7 +1270,19 @@
         localChangesGrid.resetActiveCell();
         localChangesData.setItems([]);
 
-        return $.ajax({
+        // load the necessary details
+        var loadMessage = getVersionControlInformation(processGroupId).done(function (response) {
+            if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                var vci = response.versionControlInformation;
+                localChangesMessage.text('The following changes have been made to ' + vci.flowName + ' (Version ' + vci.version + ').');
+            } else {
+                nfDialog.showOkDialog({
+                    headerText: 'Change Version',
+                    dialogContent: 'This Process Group is not currently under version control.'
+                });
+            }
+        });
+        var loadChanges = $.ajax({
             type: 'GET',
             url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/local-modifications',
             dataType: 'json'
@@ -1292,6 +1319,8 @@
                 });
             }
         }).fail(nfErrorHandler.handleAjaxError);
+
+        return $.when(loadMessage, loadChanges);
     };
 
     /**
@@ -1593,7 +1622,7 @@
             // init the show local changes dialog
             $('#show-local-changes-dialog').modal({
                 scrollableContentStyle: 'scrollable',
-                headerText: 'Local Changes',
+                headerText: 'Show Local Changes',
                 buttons: [{
                     buttonText: 'Close',
                     color: {
@@ -1711,7 +1740,7 @@
          * @param processGroupId
          */
         revertLocalChanges: function (processGroupId) {
-            loadLocalChanges(processGroupId, $('#revert-local-changes-table'), $('#total-revert-local-changes-entries')).done(function () {
+            loadLocalChanges(processGroupId, $('#revert-local-changes-message'), $('#revert-local-changes-table'), $('#total-revert-local-changes-entries')).done(function () {
                 $('#revert-local-changes-process-group-id').text(processGroupId);
                 $('#revert-local-changes-dialog').modal('show');
             });
@@ -1723,7 +1752,7 @@
          * @param processGroupId
          */
         showLocalChanges: function (processGroupId) {
-            loadLocalChanges(processGroupId, $('#show-local-changes-table'), $('#total-show-local-changes-entries')).done(function () {
+            loadLocalChanges(processGroupId, $('#show-local-changes-message'), $('#show-local-changes-table'), $('#total-show-local-changes-entries')).done(function () {
                 $('#show-local-changes-dialog').modal('show');
             });
         },

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
index 0bc6b80..66c5542 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-port.js
@@ -280,6 +280,17 @@
                             }
                         });
 
+                    // --------
+                    // comments
+                    // --------
+
+                    details.append('path')
+                        .attr({
+                            'class': 'component-comments',
+                            'transform': 'translate(' + (portData.dimensions.width - 2) + ', ' + (portData.dimensions.height - 10) + ')',
+                            'd': 'm0,0 l0,8 l-8,0 z'
+                        });
+
                     // -------------------
                     // active thread count
                     // -------------------
@@ -321,9 +332,43 @@
                         }).append('title').text(function (d) {
                         return d.component.name;
                     });
+
+                    // update the port comments
+                    port.select('path.component-comments')
+                        .style('visibility', nfCommon.isBlank(portData.component.comments) ? 'hidden' : 'visible')
+                        .each(function () {
+                            // get the tip
+                            var tip = d3.select('#comments-tip-' + portData.id);
+
+                            // if there are validation errors generate a tooltip
+                            if (nfCommon.isBlank(portData.component.comments)) {
+                                // remove the tip if necessary
+                                if (!tip.empty()) {
+                                    tip.remove();
+                                }
+                            } else {
+                                // create the tip if necessary
+                                if (tip.empty()) {
+                                    tip = d3.select('#port-tooltips').append('div')
+                                        .attr('id', function () {
+                                            return 'comments-tip-' + portData.id;
+                                        })
+                                        .attr('class', 'tooltip nifi-tooltip');
+                                }
+
+                                // update the tip
+                                tip.text(portData.component.comments);
+
+                                // add the tooltip
+                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                            }
+                        });
                 } else {
                     // clear the port name
                     port.select('text.port-name').text(null);
+
+                    // clear the port comments
+                    port.select('path.component-comments').style('visibility', false);
                 }
 
                 // populate the stats
@@ -519,6 +564,7 @@
             // remove any associated tooltips
             $('#run-status-tip-' + d.id).remove();
             $('#bulletin-tip-' + d.id).remove();
+            $('#comments-tip-' + d.id).remove();
         });
     };
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 7a61363..69db138 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -86,19 +86,6 @@
     // --------------------------
 
     /**
-     * Gets the process group comments.
-     *
-     * @param {object} d
-     */
-    var getProcessGroupComments = function (d) {
-        if (nfCommon.isBlank(d.component.comments)) {
-            return 'No comments specified';
-        } else {
-            return d.component.comments;
-        }
-    };
-
-    /**
      * Determines whether the specified process group is under version control.
      *
      * @param d
@@ -296,6 +283,19 @@
                             'fill': '#e3e8eb'
                         });
 
+                    details.append('rect')
+                        .attr({
+                            'x': 0,
+                            'y': function () {
+                                return processGroupData.dimensions.height - 24;
+                            },
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 24,
+                            'fill': '#e3e8eb'
+                        });
+
                     // --------
                     // contents
                     // --------
@@ -397,6 +397,87 @@
                             'class': 'process-group-disabled-count process-group-contents-count'
                         });
 
+                    // current icon
+                    details.append('text')
+                        .attr({
+                            'x': 10,
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-up-to-date process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf00c');
+
+                    // current count
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-up-to-date-count process-group-contents-count'
+                        });
+
+                    // modified icon
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf069');
+
+                    // modified count
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified-count process-group-contents-count'
+                        });
+
+                    // not current icon
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-stale process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf0aa');
+
+                    // not current count
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-stale-count process-group-contents-count'
+                        });
+
+                    // modified and not current icon
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified-and-stale process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf06a');
+
+                    // modified and not current count
+                    details.append('text')
+                        .attr({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified-and-stale-count process-group-contents-count'
+                        });
+
                     // ----------------
                     // stats background
                     // ----------------
@@ -676,14 +757,11 @@
                     // comments
                     // --------
 
-                    // process group comments
-                    details.append('text')
+                    details.append('path')
                         .attr({
-                            'x': 10,
-                            'y': 160,
-                            'width': 342,
-                            'height': 22,
-                            'class': 'process-group-comments'
+                            'class': 'component-comments',
+                            'transform': 'translate(' + (processGroupData.dimensions.width - 2) + ', ' + (processGroupData.dimensions.height - 10) + ')',
+                            'd': 'm0,0 l0,8 l-8,0 z'
                         });
 
                     // -------------------
@@ -928,20 +1006,35 @@
                         });
 
                     // update the process group comments
-                    details.select('text.process-group-comments')
-                        .each(function (d) {
-                            var processGroupComments = d3.select(this);
+                    processGroup.select('path.component-comments')
+                        .style('visibility', nfCommon.isBlank(processGroupData.component.comments) ? 'hidden' : 'visible')
+                        .each(function () {
+                            // get the tip
+                            var tip = d3.select('#comments-tip-' + processGroupData.id);
 
-                            // reset the process group name to handle any previous state
-                            processGroupComments.text(null).selectAll('tspan, title').remove();
-
-                            // apply ellipsis to the port name as necessary
-                            nfCanvasUtils.ellipsis(processGroupComments, getProcessGroupComments(d));
-                        }).classed('unset', function (d) {
-                        return nfCommon.isBlank(d.component.comments);
-                    }).append('title').text(function (d) {
-                        return getProcessGroupComments(d);
-                    });
+                            // if there are validation errors generate a tooltip
+                            if (nfCommon.isBlank(processGroupData.component.comments)) {
+                                // remove the tip if necessary
+                                if (!tip.empty()) {
+                                    tip.remove();
+                                }
+                            } else {
+                                // create the tip if necessary
+                                if (tip.empty()) {
+                                    tip = d3.select('#process-group-tooltips').append('div')
+                                        .attr('id', function () {
+                                            return 'comments-tip-' + processGroupData.id;
+                                        })
+                                        .attr('class', 'tooltip nifi-tooltip');
+                                }
+
+                                // update the tip
+                                tip.text(processGroupData.component.comments);
+
+                                // add the tooltip
+                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                            }
+                        });
 
                     // update the process group name
                     processGroup.select('text.process-group-name')
@@ -977,12 +1070,102 @@
                         .text(function (d) {
                             return d.component.name;
                         });
+
+                    // up to date current
+                    var upToDate = details.select('text.process-group-up-to-date')
+                        .classed('up-to-date', function (d) {
+                            return d.component.upToDateCount > 0;
+                        })
+                        .classed('zero', function (d) {
+                            return d.component.upToDateCount === 0;
+                        });
+                    var upToDateCount = details.select('text.process-group-up-to-date-count')
+                        .attr('x', function () {
+                            var currentCountX = parseInt(upToDate.attr('x'), 10);
+                            return currentCountX + Math.round(upToDate.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                        })
+                        .text(function (d) {
+                            return d.component.upToDateCount;
+                        });
+
+                    // update locally modified
+                    var locallyModified = details.select('text.process-group-locally-modified')
+                        .classed('locally-modified', function (d) {
+                            return d.component.locallyModifiedCount > 0;
+                        })
+                        .classed('zero', function (d) {
+                            return d.component.locallyModifiedCount === 0;
+                        })
+                        .attr('x', function () {
+                            var currentX = parseInt(upToDateCount.attr('x'), 10);
+                            return currentX + Math.round(upToDateCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                        });
+                    var locallyModifiedCount = details.select('text.process-group-locally-modified-count')
+                        .attr('x', function () {
+                            var modifiedCountX = parseInt(locallyModified.attr('x'), 10);
+                            return modifiedCountX + Math.round(locallyModified.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                        })
+                        .text(function (d) {
+                            return d.component.locallyModifiedCount;
+                        });
+
+                    // update stale
+                    var stale = details.select('text.process-group-stale')
+                        .classed('stale', function (d) {
+                            return d.component.staleCount > 0;
+                        })
+                        .classed('zero', function (d) {
+                            return d.component.staleCount === 0;
+                        })
+                        .attr('x', function () {
+                            var modifiedX = parseInt(locallyModifiedCount.attr('x'), 10);
+                            return modifiedX + Math.round(locallyModifiedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                        });
+                    var staleCount = details.select('text.process-group-stale-count')
+                        .attr('x', function () {
+                            var notCurrentCountX = parseInt(stale.attr('x'), 10);
+                            return notCurrentCountX + Math.round(stale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                        })
+                        .text(function (d) {
+                            return d.component.staleCount;
+                        });
+
+                    // update locally modified and stale
+                    var locallyModifiedAndStale = details.select('text.process-group-locally-modified-and-stale')
+                        .classed('locally-modified-and-stale', function (d) {
+                            return d.component.locallyModifiedAndStaleCount > 0;
+                        })
+                        .classed('zero', function (d) {
+                            return d.component.locallyModifiedAndStaleCount === 0;
+                        })
+                        .attr('x', function () {
+                            var runningX = parseInt(staleCount.attr('x'), 10);
+                            return runningX + Math.round(staleCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                        });
+                    details.select('text.process-group-locally-modified-and-stale-count')
+                        .attr('x', function () {
+                            var modifiedAndNotCurrentCountX = parseInt(locallyModifiedAndStale.attr('x'), 10);
+                            return modifiedAndNotCurrentCountX + Math.round(locallyModifiedAndStale.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                        })
+                        .text(function (d) {
+                            return d.component.locallyModifiedAndStaleCount;
+                        });
                 } else {
                     // update version control information
                     processGroup.select('text.version-control').style('visibility', false).text('');
 
                     // clear the process group comments
-                    details.select('text.process-group-comments').text(null);
+                    processGroup.select('path.component-comments').style('visibility', false);
+
+                    // clear the encapsulate versioned pg counts
+                    details.select('text.process-group-up-to-date').style('visibility', false);
+                    details.select('text.process-group-up-to-date-count').style('visibility', false);
+                    details.select('text.process-group-locally-modified').style('visibility', false);
+                    details.select('text.process-group-locally-modified-count').style('visibility', false);
+                    details.select('text.process-group-stale').style('visibility', false);
+                    details.select('text.process-group-stale-count').style('visibility', false);
+                    details.select('text.process-group-locally-modified-and-stale').style('visibility', false);
+                    details.select('text.process-group-locally-modified-and-stale-count').style('visibility', false);
 
                     // clear the process group name
                     processGroup.select('text.process-group-name')
@@ -1136,6 +1319,7 @@
             // remove any associated tooltips
             $('#bulletin-tip-' + d.id).remove();
             $('#version-control-tip-' + d.id).remove();
+            $('#comments-tip-' + d.id).remove();
         });
     };
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
index 1d9b4b2..77b9af8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-processor.js
@@ -520,6 +520,17 @@
                         })
                         .text('5 min');
 
+                    // --------
+                    // comments
+                    // --------
+
+                    details.append('path')
+                        .attr({
+                            'class': 'component-comments',
+                            'transform': 'translate(' + (processorData.dimensions.width - 2) + ', ' + (processorData.dimensions.height - 10) + ')',
+                            'd': 'm0,0 l0,8 l-8,0 z'
+                        });
+
                     // -------------------
                     // active thread count
                     // -------------------
@@ -608,6 +619,37 @@
                         }).append('title').text(function (d) {
                             return nfCommon.formatBundle(d.component.bundle);
                         });
+
+                    // update the processor comments
+                    processor.select('path.component-comments')
+                        .style('visibility', nfCommon.isBlank(processorData.component.config.comments) ? 'hidden' : 'visible')
+                        .each(function () {
+                            // get the tip
+                            var tip = d3.select('#comments-tip-' + processorData.id);
+
+                            // if there are validation errors generate a tooltip
+                            if (nfCommon.isBlank(processorData.component.config.comments)) {
+                                // remove the tip if necessary
+                                if (!tip.empty()) {
+                                    tip.remove();
+                                }
+                            } else {
+                                // create the tip if necessary
+                                if (tip.empty()) {
+                                    tip = d3.select('#processor-tooltips').append('div')
+                                        .attr('id', function () {
+                                            return 'comments-tip-' + processorData.id;
+                                        })
+                                        .attr('class', 'tooltip nifi-tooltip');
+                                }
+
+                                // update the tip
+                                tip.text(processorData.component.config.comments);
+
+                                // add the tooltip
+                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                            }
+                        });
                 } else {
                     // clear the processor name
                     processor.select('text.processor-name').text(null);
@@ -617,6 +659,9 @@
 
                     // clear the processor bundle
                     processor.select('text.processor-bundle').text(null);
+
+                    // clear the processor comments
+                    processor.select('path.component-comments').style('visibility', false);
                 }
 
                 // populate the stats
@@ -903,6 +948,7 @@
             // remove any associated tooltips
             $('#run-status-tip-' + d.id).remove();
             $('#bulletin-tip-' + d.id).remove();
+            $('#comments-tip-' + d.id).remove();
         });
     };
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/d34fb5e2/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
index a71b1b8..5de0164 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-remote-process-group.js
@@ -84,19 +84,6 @@
     // --------------------------
 
     /**
-     * Gets the process group comments.
-     *
-     * @param {object} d
-     */
-    var getProcessGroupComments = function (d) {
-        if (nfCommon.isBlank(d.component.comments)) {
-            return 'No comments specified';
-        } else {
-            return d.component.comments;
-        }
-    };
-
-    /**
      * Selects the remote process group elements against the current remote process group map.
      */
     var select = function () {
@@ -182,9 +169,6 @@
         remoteProcessGroup.call(nfSelectable.activate).call(nfContextMenu.activate).call(nfQuickSelect.activate);
     };
 
-    // attempt of space between component count and icon for process group contents
-    var CONTENTS_SPACER = 5;
-
     /**
      * Updates the process groups in the specified selection.
      *
@@ -438,20 +422,6 @@
                         })
                         .text('5 min');
 
-                    // --------
-                    // comments
-                    // --------
-
-                    // process group comments
-                    details.append('text')
-                        .attr({
-                            'x': 10,
-                            'y': 121,
-                            'width': 342,
-                            'height': 22,
-                            'class': 'remote-process-group-comments'
-                        });
-
                     // -------------------
                     // last refreshed time
                     // -------------------
@@ -471,11 +441,22 @@
 
                     details.append('text')
                         .attr({
-                            'x': 370,
+                            'x': 10,
                             'y': 150,
                             'class': 'remote-process-group-last-refresh'
                         });
 
+                    // --------
+                    // comments
+                    // --------
+
+                    details.append('path')
+                        .attr({
+                            'class': 'component-comments',
+                            'transform': 'translate(' + (remoteProcessGroupData.dimensions.width - 2) + ', ' + (remoteProcessGroupData.dimensions.height - 10) + ')',
+                            'd': 'm0,0 l0,8 l-8,0 z'
+                        });
+
                     // -------------------
                     // active thread count
                     // -------------------
@@ -579,21 +560,36 @@
                     // update comments
                     // ---------------
 
-                    // update the process group comments
-                    details.select('text.remote-process-group-comments')
-                        .each(function (d) {
-                            var remoteProcessGroupComments = d3.select(this);
+                    // update the remote process group comments
+                    details.select('path.component-comments')
+                        .style('visibility', nfCommon.isBlank(remoteProcessGroupData.component.comments) ? 'hidden' : 'visible')
+                        .each(function () {
+                            // get the tip
+                            var tip = d3.select('#comments-tip-' + remoteProcessGroupData.id);
+
+                            // if there are validation errors generate a tooltip
+                            if (nfCommon.isBlank(remoteProcessGroupData.component.comments)) {
+                                // remove the tip if necessary
+                                if (!tip.empty()) {
+                                    tip.remove();
+                                }
+                            } else {
+                                // create the tip if necessary
+                                if (tip.empty()) {
+                                    tip = d3.select('#remote-process-group-tooltips').append('div')
+                                        .attr('id', function () {
+                                            return 'comments-tip-' + remoteProcessGroupData.id;
+                                        })
+                                        .attr('class', 'tooltip nifi-tooltip');
+                                }
 
-                            // reset the processor name to handle any previous state
-                            remoteProcessGroupComments.text(null).selectAll('tspan, title').remove();
+                                // update the tip
+                                tip.text(remoteProcessGroupData.component.comments);
 
-                            // apply ellipsis to the port name as necessary
-                            nfCanvasUtils.ellipsis(remoteProcessGroupComments, getProcessGroupComments(d));
-                        }).classed('unset', function (d) {
-                        return nfCommon.isBlank(d.component.comments);
-                    }).append('title').text(function (d) {
-                        return getProcessGroupComments(d);
-                    });
+                                // add the tooltip
+                                nfCanvasUtils.canvasTooltip(tip, d3.select(this));
+                            }
+                        });
 
                     // --------------
                     // last refreshed
@@ -628,8 +624,8 @@
                     // clear the transmission secure icon
                     details.select('text.remote-process-group-transmission-secure').text(null);
 
-                    // clear the process group comments
-                    details.select('text.remote-process-group-comments').text(null);
+                    // clear the comments
+                    details.select('path.component-comments').style('visibility', false);
 
                     // clear the last refresh
                     details.select('text.remote-process-group-last-refresh').text(null);
@@ -853,6 +849,7 @@
             $('#bulletin-tip-' + d.id).remove();
             $('#authorization-issues-' + d.id).remove();
             $('#transmission-secure-' + d.id).remove();
+            $('#comments-tip-' + d.id).remove();
         });
     };
 


[50/50] nifi git commit: NIFI-4436: Ensure that on save, we assign a Versioned Component Identifier to inner process groups that are tracking to remote flows, if they don't have one. This would occur, for instance, if a Process Group was imported into an

Posted by bb...@apache.org.
NIFI-4436: Ensure that on save, we assign a Versioned Component Identifier to inner process groups that are tracking to remote flows, if they don't have one. This would occur, for instance, if a Process Group was imported into an existing group (or copied/moved into it) and then the existing group was saved.

NIFI-4436: Fixed a bug that caused a flow not to successfully change version if a connection is added to an existing component and that component is running at time of version change

NIFI-4436: Fixed bug with ordering of controller services being enabled and disabled

NIFI-4436: Fixed bug that prevented local input and output ports from being stopped and started as needed

NIFI-4436: Fixed bugs around referencing controller services that are at a higher level than the versioned flow

NIFI-4436: Ensure that we clear components from FlowController's cache when removed and that they are added to cache when created.

NIFI-4436: Fixed error message coming back if component is invalid when trying to be restarted/re-enabled

NIFI-4436: Addressed issue with children of a removed process group not being considered 'affected components' and as a result not being stopped/disabled/restarted/re-enabled

This closes #2219.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/b6117743
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/b6117743
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/b6117743

Branch: refs/heads/master
Commit: b6117743d4c1c1a37a16ba746b9edbbdd276d69f
Parents: fa996cd
Author: Mark Payne <ma...@hotmail.com>
Authored: Thu Jan 4 16:09:02 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 13:10:13 2018 -0500

----------------------------------------------------------------------
 .../nifi/web/api/dto/AffectedComponentDTO.java  |   3 +
 .../service/ControllerServiceProvider.java      |  22 ++-
 .../apache/nifi/controller/FlowController.java  |  11 ++
 .../StandardControllerServiceProvider.java      | 127 ++++++++++++++-
 .../nifi/groups/StandardProcessGroup.java       |  42 ++---
 .../TestStandardProcessScheduler.java           |  17 +-
 .../TestStandardControllerServiceProvider.java  |   9 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  21 ++-
 .../apache/nifi/web/ResumeFlowException.java    |  31 ++++
 .../nifi/web/StandardNiFiServiceFacade.java     | 160 ++++++++++++++++---
 .../apache/nifi/web/api/VersionsResource.java   |  33 +++-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  27 ++++
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |   7 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |  54 +++----
 .../nifi/web/util/AffectedComponentUtils.java   |   9 ++
 .../nifi/web/util/LocalComponentLifecycle.java  |  45 ++++++
 16 files changed, 520 insertions(+), 98 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
index 95024ca..dd7c6c4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
@@ -26,6 +26,8 @@ import java.util.Collection;
 public class AffectedComponentDTO {
     public static final String COMPONENT_TYPE_PROCESSOR = "PROCESSOR";
     public static final String COMPONENT_TYPE_CONTROLLER_SERVICE = "CONTROLLER_SERVICE";
+    public static final String COMPONENT_TYPE_INPUT_PORT = "INPUT_PORT";
+    public static final String COMPONENT_TYPE_OUTPUT_PORT = "OUTPUT_PORT";
     public static final String COMPONENT_TYPE_REMOTE_INPUT_PORT = "REMOTE_INPUT_PORT";
     public static final String COMPONENT_TYPE_REMOTE_OUTPUT_PORT = "REMOTE_OUTPUT_PORT";
 
@@ -58,6 +60,7 @@ public class AffectedComponentDTO {
 
     @ApiModelProperty(value = "The type of this component",
         allowableValues = COMPONENT_TYPE_PROCESSOR + "," + COMPONENT_TYPE_CONTROLLER_SERVICE + ", "
+            + COMPONENT_TYPE_INPUT_PORT + ", " + COMPONENT_TYPE_OUTPUT_PORT + ", "
             + COMPONENT_TYPE_REMOTE_INPUT_PORT + ", " + COMPONENT_TYPE_REMOTE_OUTPUT_PORT)
     public String getReferenceType() {
         return referenceType;

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java
index 010ecdf..ae5416c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java
@@ -20,6 +20,7 @@ import java.net.URL;
 import java.util.Collection;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
 
 import org.apache.nifi.annotation.lifecycle.OnAdded;
 import org.apache.nifi.bundle.BundleCoordinate;
@@ -84,6 +85,16 @@ public interface ControllerServiceProvider extends ControllerServiceLookup {
     void enableControllerServices(Collection<ControllerServiceNode> serviceNodes);
 
     /**
+     * Enables the collection of services in the background. If a service in this collection
+     * depends on another service, the service being depended on must either already be enabled
+     * or must be in the collection as well.
+     *
+     * @param serviceNodes the nodes
+     * @return a Future that can be used to cancel the task or wait until it is completed
+     */
+    Future<Void> enableControllerServicesAsync(Collection<ControllerServiceNode> serviceNodes);
+
+    /**
      * Disables the given controller service so that it cannot be used by other
      * components. This allows configuration to be updated or allows service to
      * be removed.
@@ -93,8 +104,17 @@ public interface ControllerServiceProvider extends ControllerServiceLookup {
     CompletableFuture<Void> disableControllerService(ControllerServiceNode serviceNode);
 
     /**
+     * Disables the collection of services in the background. If any of the services given is referenced
+     * by another service, then that other service must either be disabled or be in the given collection.
+     *
+     * @param serviceNodes the nodes the disable
+     * @return a Future that can be used to cancel the task or wait until it is completed
+     */
+    Future<Void> disableControllerServicesAsync(Collection<ControllerServiceNode> serviceNodes);
+
+    /**
      * @return a Set of all Controller Services that exist for this service
-     * provider
+     *         provider
      */
     Set<ControllerServiceNode> getAllControllerServices();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index eb4b8b9..88dc11c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -245,6 +245,7 @@ import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -3610,12 +3611,22 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
     }
 
     @Override
+    public Future<Void> enableControllerServicesAsync(final Collection<ControllerServiceNode> serviceNodes) {
+        return controllerServiceProvider.enableControllerServicesAsync(serviceNodes);
+    }
+
+    @Override
     public CompletableFuture<Void> disableControllerService(final ControllerServiceNode serviceNode) {
         serviceNode.verifyCanDisable();
         return controllerServiceProvider.disableControllerService(serviceNode);
     }
 
     @Override
+    public Future<Void> disableControllerServicesAsync(final Collection<ControllerServiceNode> serviceNodes) {
+        return controllerServiceProvider.disableControllerServicesAsync(serviceNodes);
+    }
+
+    @Override
     public void verifyCanEnableReferencingServices(final ControllerServiceNode serviceNode) {
         controllerServiceProvider.verifyCanEnableReferencingServices(serviceNode);
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
index 48ad849..b4d9e8b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
@@ -35,6 +35,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.ClassUtils;
@@ -50,13 +51,13 @@ import org.apache.nifi.controller.ConfiguredComponent;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.LoggableComponent;
-import org.apache.nifi.controller.ProcessScheduler;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.ValidationContextFactory;
 import org.apache.nifi.controller.exception.ComponentLifeCycleException;
 import org.apache.nifi.controller.exception.ControllerServiceInstantiationException;
+import org.apache.nifi.controller.scheduling.StandardProcessScheduler;
 import org.apache.nifi.events.BulletinFactory;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.logging.ComponentLog;
@@ -78,7 +79,7 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
 
     private static final Logger logger = LoggerFactory.getLogger(StandardControllerServiceProvider.class);
 
-    private final ProcessScheduler processScheduler;
+    private final StandardProcessScheduler processScheduler;
     private final BulletinRepository bulletinRepo;
     private final StateManagerProvider stateManagerProvider;
     private final VariableRegistry variableRegistry;
@@ -87,7 +88,7 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
 
     private final ConcurrentMap<String, ControllerServiceNode> serviceCache = new ConcurrentHashMap<>();
 
-    public StandardControllerServiceProvider(final FlowController flowController, final ProcessScheduler scheduler, final BulletinRepository bulletinRepo,
+    public StandardControllerServiceProvider(final FlowController flowController, final StandardProcessScheduler scheduler, final BulletinRepository bulletinRepo,
             final StateManagerProvider stateManagerProvider, final VariableRegistry variableRegistry, final NiFiProperties nifiProperties) {
 
         this.flowController = flowController;
@@ -384,6 +385,74 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
         }
     }
 
+    @Override
+    public Future<Void> enableControllerServicesAsync(final Collection<ControllerServiceNode> serviceNodes) {
+        final CompletableFuture<Void> future = new CompletableFuture<>();
+        processScheduler.submitFrameworkTask(() -> {
+            enableControllerServices(serviceNodes, future);
+            future.complete(null);
+        });
+
+        return future;
+    }
+
+    private void enableControllerServices(final Collection<ControllerServiceNode> serviceNodes, final CompletableFuture<Void> completableFuture) {
+        // validate that we are able to start all of the services.
+        Iterator<ControllerServiceNode> serviceIter = serviceNodes.iterator();
+        while (serviceIter.hasNext()) {
+            ControllerServiceNode controllerServiceNode = serviceIter.next();
+            List<ControllerServiceNode> requiredServices = ((StandardControllerServiceNode) controllerServiceNode).getRequiredControllerServices();
+            for (ControllerServiceNode requiredService : requiredServices) {
+                if (!requiredService.isActive() && !serviceNodes.contains(requiredService)) {
+                    logger.error("Cannot enable {} because it has a dependency on {}, which is not enabled", controllerServiceNode, requiredService);
+                    completableFuture.completeExceptionally(new IllegalStateException("Cannot enable " + controllerServiceNode
+                        + " because it has a dependency on " + requiredService + ", which is not enabled"));
+                    return;
+                }
+            }
+        }
+
+        for (final ControllerServiceNode controllerServiceNode : serviceNodes) {
+            if (completableFuture.isCancelled()) {
+                return;
+            }
+
+            try {
+                if (!controllerServiceNode.isActive()) {
+                    final Future<Void> future = enableControllerServiceDependenciesFirst(controllerServiceNode);
+
+                    while (true) {
+                        try {
+                            future.get(1, TimeUnit.SECONDS);
+                            logger.debug("Successfully enabled {}; service state = {}", controllerServiceNode, controllerServiceNode.getState());
+                            break;
+                        } catch (final TimeoutException e) {
+                            if (completableFuture.isCancelled()) {
+                                return;
+                            }
+                        } catch (final Exception e) {
+                            logger.warn("Failed to enable service {}", controllerServiceNode, e);
+                            completableFuture.completeExceptionally(e);
+
+                            if (this.bulletinRepo != null) {
+                                this.bulletinRepo.addBulletin(BulletinFactory.createBulletin("Controller Service",
+                                    Severity.ERROR.name(), "Could not enable " + controllerServiceNode + " due to " + e));
+                            }
+
+                            return;
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                logger.error("Failed to enable " + controllerServiceNode, e);
+                if (this.bulletinRepo != null) {
+                    this.bulletinRepo.addBulletin(BulletinFactory.createBulletin("Controller Service",
+                        Severity.ERROR.name(), "Could not start " + controllerServiceNode + " due to " + e));
+                }
+            }
+        }
+    }
+
     private Future<Void> enableControllerServiceDependenciesFirst(ControllerServiceNode serviceNode) {
         final Map<ControllerServiceNode, Future<Void>> futures = new HashMap<>();
 
@@ -461,6 +530,58 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi
     }
 
     @Override
+    public Future<Void> disableControllerServicesAsync(final Collection<ControllerServiceNode> serviceNodes) {
+        final CompletableFuture<Void> future = new CompletableFuture<>();
+        processScheduler.submitFrameworkTask(() -> {
+            disableControllerServices(serviceNodes, future);
+            future.complete(null);
+        });
+
+        return future;
+    }
+
+    private void disableControllerServices(final Collection<ControllerServiceNode> serviceNodes, final CompletableFuture<Void> future) {
+        final Set<ControllerServiceNode> serviceNodeSet = new HashSet<>(serviceNodes);
+
+        // Verify that for each Controller Service given, any service that references it is either disabled or is also in the given collection
+        for (final ControllerServiceNode serviceNode : serviceNodes) {
+            final List<ControllerServiceNode> references = serviceNode.getReferences().findRecursiveReferences(ControllerServiceNode.class);
+            for (final ControllerServiceNode reference : references) {
+                if (reference.isActive()) {
+                    try {
+                        reference.verifyCanDisable(serviceNodeSet);
+                    } catch (final Exception e) {
+                        future.completeExceptionally(e);
+                    }
+                }
+            }
+        }
+
+        for (final ControllerServiceNode serviceNode : serviceNodes) {
+            if (serviceNode.isActive()) {
+                disableReferencingServices(serviceNode);
+                final CompletableFuture<?> serviceFuture = disableControllerService(serviceNode);
+
+                while (true) {
+                    try {
+                        serviceFuture.get(1, TimeUnit.SECONDS);
+                        break;
+                    } catch (final TimeoutException e) {
+                        if (future.isCancelled()) {
+                            return;
+                        }
+
+                        continue;
+                    } catch (final Exception e) {
+                        logger.error("Failed to disable {}", serviceNode, e);
+                        future.completeExceptionally(e);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
     public ControllerService getControllerService(final String serviceIdentifier) {
         final ControllerServiceNode node = getControllerServiceNode(serviceIdentifier);
         return node == null ? null : node.getProxiedControllerService();

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 9418f40..4cd6e2a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -738,7 +738,8 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
 
         for (final ControllerServiceNode cs : group.getControllerServices(false)) {
-            group.removeControllerService(cs);
+            // Must go through Controller Service here because we need to ensure that it is removed from the cache
+            flowController.removeControllerService(cs);
         }
 
         for (final ProcessGroup childGroup : new ArrayList<>(group.getProcessGroups())) {
@@ -3158,9 +3159,13 @@ public final class StandardProcessGroup implements ProcessGroup {
                     .forEach(port -> port.setVersionedComponentId(lookup.apply(port.getIdentifier())));
             });
 
-        processGroup.getProcessGroups().stream()
-            .filter(childGroup -> childGroup.getVersionControlInformation() == null)
-            .forEach(childGroup -> applyVersionedComponentIds(childGroup, lookup));
+        for (final ProcessGroup childGroup : processGroup.getProcessGroups()) {
+            if (childGroup.getVersionControlInformation() == null) {
+                applyVersionedComponentIds(childGroup, lookup);
+            } else if (!childGroup.getVersionedComponentId().isPresent()) {
+                childGroup.setVersionedComponentId(lookup.apply(childGroup.getIdentifier()));
+            }
+        }
     }
 
 
@@ -3242,7 +3247,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", versionedGroup);
             final ComparableDataFlow remoteFlow = new StandardComparableDataFlow("Remote Flow", proposedSnapshot.getFlowContents());
 
-            final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow, getAncestorGroupServiceIds(), new StaticDifferenceDescriptor());
+            final FlowComparator flowComparator = new StandardFlowComparator(remoteFlow, localFlow, getAncestorGroupServiceIds(), new StaticDifferenceDescriptor());
             final FlowComparison flowComparison = flowComparator.compare();
 
             final Set<String> updatedVersionedComponentIds = new HashSet<>();
@@ -3387,7 +3392,6 @@ public final class StandardProcessGroup implements ProcessGroup {
             .map(VariableDescriptor::getName)
             .collect(Collectors.toSet());
 
-
         final Map<String, String> updatedVariableMap = new HashMap<>();
 
         // If any new variables exist in the proposed flow, add those to the variable registry.
@@ -3477,6 +3481,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
             if (childGroup == null) {
                 final ProcessGroup added = addProcessGroup(group, proposedChildGroup, componentIdSeed, variablesToSkip);
+                flowController.onProcessGroupAdded(added);
                 added.findAllRemoteProcessGroups().stream().forEach(RemoteProcessGroup::initialize);
                 LOG.info("Added {} to {}", added, this);
             } else if (childCoordinates == null || updateDescendantVersionedGroups) {
@@ -3496,6 +3501,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final Funnel funnel = funnelsByVersionedId.get(proposedFunnel.getIdentifier());
             if (funnel == null) {
                 final Funnel added = addFunnel(group, proposedFunnel, componentIdSeed);
+                flowController.onFunnelAdded(added);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedFunnel.getIdentifier())) {
                 updateFunnel(funnel, proposedFunnel);
@@ -3517,6 +3523,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final Port port = inputPortsByVersionedId.get(proposedPort.getIdentifier());
             if (port == null) {
                 final Port added = addInputPort(group, proposedPort, componentIdSeed);
+                flowController.onInputPortAdded(added);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedPort.getIdentifier())) {
                 updatePort(port, proposedPort);
@@ -3537,6 +3544,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final Port port = outputPortsByVersionedId.get(proposedPort.getIdentifier());
             if (port == null) {
                 final Port added = addOutputPort(group, proposedPort, componentIdSeed);
+                flowController.onOutputPortAdded(added);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedPort.getIdentifier())) {
                 updatePort(port, proposedPort);
@@ -3580,6 +3588,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final ProcessorNode processor = processorsByVersionedId.get(proposedProcessor.getIdentifier());
             if (processor == null) {
                 final ProcessorNode added = addProcessor(group, proposedProcessor, componentIdSeed);
+                flowController.onProcessorAdded(added);
 
                 final Set<Relationship> proposedAutoTerminated =
                     proposedProcessor.getAutoTerminatedRelationships() == null ? Collections.emptySet() : proposedProcessor.getAutoTerminatedRelationships().stream()
@@ -3638,6 +3647,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final Connection connection = connectionsByVersionedId.get(proposedConnection.getIdentifier());
             if (connection == null) {
                 final Connection added = addConnection(group, proposedConnection, componentIdSeed);
+                flowController.onConnectionAdded(added);
                 LOG.info("Added {} to {}", added, this);
             } else if (isUpdateable(connection)) {
                 // If the connection needs to be updated, then the source and destination will already have
@@ -3658,6 +3668,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             final Connection connection = connectionsByVersionedId.get(removedVersionedId);
             LOG.info("Removing {} from {}", connection, group);
             group.removeConnection(connection);
+            flowController.onConnectionRemoved(connection);
         }
 
         // Once the appropriate connections have been removed, we may now update Processors' auto-terminated relationships.
@@ -3670,7 +3681,8 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final String removedVersionedId : controllerServicesRemoved) {
             final ControllerServiceNode service = servicesByVersionedId.get(removedVersionedId);
             LOG.info("Removing {} from {}", service, group);
-            group.removeControllerService(service);
+            // Must remove Controller Service through Flow Controller in order to remove from cache
+            flowController.removeControllerService(service);
         }
 
         for (final String removedVersionedId : funnelsRemoved) {
@@ -4065,13 +4077,6 @@ public final class StandardProcessGroup implements ProcessGroup {
                     // to the instance ID of the Controller Service.
                     final String serviceVersionedComponentId = entry.getValue();
                     String instanceId = getServiceInstanceId(serviceVersionedComponentId, group);
-                    if (instanceId == null) {
-                        // We didn't find the instance ID based on the Versioned Component ID. So we want to just
-                        // leave the value set to whatever it currently is, if it's currently set.
-                        final PropertyDescriptor propertyDescriptor = new PropertyDescriptor.Builder().name(entry.getKey()).build();
-                        instanceId = currentProperties.get(propertyDescriptor);
-                    }
-
                     value = instanceId == null ? serviceVersionedComponentId : instanceId;
                 } else {
                     value = entry.getValue();
@@ -4085,13 +4090,9 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private String getServiceInstanceId(final String serviceVersionedComponentId, final ProcessGroup group) {
-        for (final ControllerServiceNode serviceNode : group.getControllerServices(false)) {
+        for (final ControllerServiceNode serviceNode : group.getControllerServices(true)) {
             final Optional<String> optionalVersionedId = serviceNode.getVersionedComponentId();
-            if (!optionalVersionedId.isPresent()) {
-                continue;
-            }
-
-            final String versionedId = optionalVersionedId.get();
+            final String versionedId = optionalVersionedId.orElseGet(() -> UUID.nameUUIDFromBytes(serviceNode.getIdentifier().getBytes(StandardCharsets.UTF_8)).toString());
             if (versionedId.equals(serviceVersionedComponentId)) {
                 return serviceNode.getIdentifier();
             }
@@ -4319,7 +4320,6 @@ public final class StandardProcessGroup implements ProcessGroup {
                 }
             }
 
-
             // Ensure that all Processors are instantiate-able.
             final Map<String, VersionedProcessor> proposedProcessors = new HashMap<>();
             findAllProcessors(updatedFlow.getFlowContents(), proposedProcessors);

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
index 314738a..c0b36c9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/TestStandardProcessScheduler.java
@@ -47,7 +47,6 @@ import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.LoggableComponent;
-import org.apache.nifi.controller.ProcessScheduler;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReloadComponent;
 import org.apache.nifi.controller.ReportingTaskNode;
@@ -269,7 +268,7 @@ public class TestStandardProcessScheduler {
      */
     @Test
     public void validateServiceEnablementLogicHappensOnlyOnce() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ControllerServiceNode serviceNode = provider.createControllerService(SimpleTestService.class.getName(),
                 "1", systemBundle.getBundleDetails().getCoordinate(), null, false);
@@ -308,7 +307,7 @@ public class TestStandardProcessScheduler {
      */
     @Test
     public void validateDisabledServiceCantBeDisabled() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ControllerServiceNode serviceNode = provider.createControllerService(SimpleTestService.class.getName(),
                 "1", systemBundle.getBundleDetails().getCoordinate(), null, false);
@@ -346,7 +345,7 @@ public class TestStandardProcessScheduler {
      */
     @Test
     public void validateEnabledServiceCanOnlyBeDisabledOnce() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ControllerServiceNode serviceNode = provider.createControllerService(SimpleTestService.class.getName(),
                 "1", systemBundle.getBundleDetails().getCoordinate(), null, false);
@@ -380,7 +379,7 @@ public class TestStandardProcessScheduler {
 
     @Test
     public void validateDisablingOfTheFailedService() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ControllerServiceNode serviceNode = provider.createControllerService(FailingService.class.getName(),
                 "1", systemBundle.getBundleDetails().getCoordinate(), null, false);
@@ -412,7 +411,7 @@ public class TestStandardProcessScheduler {
     @Test
     @Ignore
     public void validateEnabledDisableMultiThread() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ExecutorService executor = Executors.newCachedThreadPool();
         for (int i = 0; i < 200; i++) {
@@ -455,7 +454,7 @@ public class TestStandardProcessScheduler {
      */
     @Test
     public void validateNeverEnablingServiceCanStillBeDisabled() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ControllerServiceNode serviceNode = provider.createControllerService(LongEnablingService.class.getName(),
                 "1", systemBundle.getBundleDetails().getCoordinate(), null, false);
@@ -481,7 +480,7 @@ public class TestStandardProcessScheduler {
      */
     @Test
     public void validateLongEnablingServiceCanStillBeDisabled() throws Exception {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties);
         final ControllerServiceNode serviceNode = provider.createControllerService(LongEnablingService.class.getName(),
                 "1", systemBundle.getBundleDetails().getCoordinate(), null, false);
@@ -581,7 +580,7 @@ public class TestStandardProcessScheduler {
         }
     }
 
-    private ProcessScheduler createScheduler() {
+    private StandardProcessScheduler createScheduler() {
         return new StandardProcessScheduler(null, null, stateMgrProvider, nifiProperties);
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
index 0d15143..ed335e9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/TestStandardControllerServiceProvider.java
@@ -38,7 +38,6 @@ import org.apache.nifi.components.state.StateManager;
 import org.apache.nifi.components.state.StateManagerProvider;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.LoggableComponent;
-import org.apache.nifi.controller.ProcessScheduler;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReloadComponent;
 import org.apache.nifi.controller.ScheduledState;
@@ -146,7 +145,7 @@ public class TestStandardControllerServiceProvider {
         final FlowController controller = Mockito.mock(FlowController.class);
         Mockito.when(controller.getGroup(Mockito.anyString())).thenReturn(procGroup);
 
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider =
                 new StandardControllerServiceProvider(controller, scheduler, null, stateManagerProvider, variableRegistry, niFiProperties);
 
@@ -162,7 +161,7 @@ public class TestStandardControllerServiceProvider {
         final FlowController controller = Mockito.mock(FlowController.class);
         Mockito.when(controller.getGroup(Mockito.anyString())).thenReturn(group);
 
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         final StandardControllerServiceProvider provider =
                 new StandardControllerServiceProvider(controller, scheduler, null, stateManagerProvider, variableRegistry, niFiProperties);
 
@@ -214,13 +213,13 @@ public class TestStandardControllerServiceProvider {
      */
     @Test(timeout = 120000)
     public void testConcurrencyWithEnablingReferencingServicesGraph() throws InterruptedException {
-        final ProcessScheduler scheduler = createScheduler();
+        final StandardProcessScheduler scheduler = createScheduler();
         for (int i = 0; i < 5000; i++) {
             testEnableReferencingServicesGraph(scheduler);
         }
     }
 
-    public void testEnableReferencingServicesGraph(final ProcessScheduler scheduler) {
+    public void testEnableReferencingServicesGraph(final StandardProcessScheduler scheduler) {
         final ProcessGroup procGroup = new MockProcessGroup(controller);
         final FlowController controller = Mockito.mock(FlowController.class);
         Mockito.when(controller.getGroup(Mockito.anyString())).thenReturn(procGroup);

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index ab96747..165af45 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -116,6 +116,7 @@ import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -778,6 +779,15 @@ public interface NiFiServiceFacade {
     PortEntity getInputPort(String inputPortId);
 
     /**
+     * Gets an input port as it is available to the given user
+     *
+     * @param inputPortId The input port id
+     * @param user the user
+     * @return port
+     */
+    PortEntity getInputPort(String inputPortId, NiFiUser user);
+
+    /**
      * Gets all input ports in a given group.
      *
      * @param groupId The id of the group
@@ -847,6 +857,15 @@ public interface NiFiServiceFacade {
     PortEntity getOutputPort(String outputPortId);
 
     /**
+     * Gets an output port as it is available to the given user
+     *
+     * @param outputPortId The output port id
+     * @param user the user
+     * @return port
+     */
+    PortEntity getOutputPort(String outputPortId, NiFiUser user);
+
+    /**
      * Gets all output ports in a given group.
      *
      * @param groupId The id of the group
@@ -1008,7 +1027,7 @@ public interface NiFiServiceFacade {
      * @param state the state
      * @param serviceIds the id's of the services
      */
-    void verifyActivateControllerServices(String processGroupId, ControllerServiceState state, Set<String> serviceIds);
+    void verifyActivateControllerServices(String processGroupId, ControllerServiceState state, Collection<String> serviceIds);
 
     /**
      * Enables or disables the controller services with the given IDs & Revisions on behalf of the currently logged in user

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/ResumeFlowException.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/ResumeFlowException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/ResumeFlowException.java
new file mode 100644
index 0000000..8e1e123
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/ResumeFlowException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.web;
+
+/**
+ * Thrown whenever a flow cannot be resumed due to validation error, etc.
+ */
+public class ResumeFlowException extends Exception {
+    public ResumeFlowException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+    public ResumeFlowException(final String message) {
+        super(message);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 1ccced2..6e0e9d2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,7 +16,9 @@
  */
 package org.apache.nifi.web;
 
-import com.google.common.collect.Sets;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
@@ -271,8 +273,8 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
+import com.google.common.collect.Sets;
+
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -484,8 +486,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public void verifyActivateControllerServices(final String groupId, final ControllerServiceState state, final Set<String> serviceIds) {
-        processGroupDAO.verifyActivateControllerServices(groupId, state, serviceIds);
+    public void verifyActivateControllerServices(final String groupId, final ControllerServiceState state, final Collection<String> serviceIds) {
+        processGroupDAO.verifyActivateControllerServices(state, serviceIds);
     }
 
     @Override
@@ -1016,7 +1018,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                 @Override
                 public RevisionUpdate<ActivateControllerServicesEntity> update() {
                     // schedule the components
-                    processGroupDAO.activateControllerServices(processGroupId, state, serviceRevisions.keySet());
+                    processGroupDAO.activateControllerServices(state, serviceRevisions.keySet());
 
                     // update the revisions
                     final Map<String, Revision> updatedRevisions = new HashMap<>();
@@ -3289,8 +3291,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     private PortEntity createInputPortEntity(final Port port) {
+        return createInputPortEntity(port, NiFiUserUtils.getNiFiUser());
+    }
+
+    private PortEntity createInputPortEntity(final Port port, final NiFiUser user) {
         final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(port.getIdentifier()));
-        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port, user);
         final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(port.getIdentifier()));
         final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier()));
         final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
@@ -3298,8 +3304,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     private PortEntity createOutputPortEntity(final Port port) {
+        return createOutputPortEntity(port, NiFiUserUtils.getNiFiUser());
+    }
+
+    private PortEntity createOutputPortEntity(final Port port, final NiFiUser user) {
         final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(port.getIdentifier()));
-        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port, user);
         final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(port.getIdentifier()));
         final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier()));
         final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
@@ -3409,6 +3419,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
+    public PortEntity getInputPort(final String inputPortId, final NiFiUser user) {
+        final Port port = inputPortDAO.getPort(inputPortId);
+        return createInputPortEntity(port, user);
+    }
+
+    @Override
     public PortStatusEntity getInputPortStatus(final String inputPortId) {
         final Port inputPort = inputPortDAO.getPort(inputPortId);
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(inputPort);
@@ -3423,6 +3439,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
+    public PortEntity getOutputPort(final String outputPortId, final NiFiUser user) {
+        final Port port = outputPortDAO.getPort(outputPortId);
+        return createOutputPortEntity(port, user);
+    }
+
+    @Override
     public PortStatusEntity getOutputPortStatus(final String outputPortId) {
         final Port outputPort = outputPortDAO.getPort(outputPortId);
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(outputPort);
@@ -3974,28 +3996,95 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             })
             .collect(Collectors.toCollection(HashSet::new));
 
-        final Map<String, List<Connection>> connectionsByVersionedId = group.findAllConnections().stream()
-            .filter(conn -> conn.getVersionedComponentId().isPresent())
-            .collect(Collectors.groupingBy(conn -> conn.getVersionedComponentId().get()));
+        for (final FlowDifference difference : comparison.getDifferences()) {
+            final VersionedComponent localComponent = difference.getComponentA();
+            if (localComponent == null) {
+                continue;
+            }
 
+            // If any Process Group is removed, consider all components below that Process Group as an affected component
+            if (difference.getDifferenceType() == DifferenceType.COMPONENT_REMOVED && localComponent.getComponentType() == org.apache.nifi.registry.flow.ComponentType.PROCESS_GROUP) {
+                final String localGroupId = ((InstantiatedVersionedProcessGroup) localComponent).getInstanceId();
+                final ProcessGroup localGroup = processGroupDAO.getProcessGroup(localGroupId);
+
+                localGroup.findAllProcessors().stream()
+                    .map(comp -> createAffectedComponentEntity(comp, user))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllFunnels().stream()
+                    .map(comp -> createAffectedComponentEntity(comp, user))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllInputPorts().stream()
+                    .map(comp -> createAffectedComponentEntity(comp, user))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllOutputPorts().stream()
+                    .map(comp -> createAffectedComponentEntity(comp, user))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllRemoteProcessGroups().stream()
+                    .flatMap(rpg -> Stream.concat(rpg.getInputPorts().stream(), rpg.getOutputPorts().stream()))
+                    .map(comp -> createAffectedComponentEntity(comp, user))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllControllerServices().stream()
+                    .map(comp -> createAffectedComponentEntity(comp, user))
+                    .forEach(affectedComponents::add);
+            }
+
+            if (localComponent.getComponentType() == org.apache.nifi.registry.flow.ComponentType.CONTROLLER_SERVICE) {
+                final String serviceId = ((InstantiatedVersionedControllerService) localComponent).getInstanceId();
+                final ControllerServiceNode serviceNode = controllerServiceDAO.getControllerService(serviceId);
+
+                final List<ControllerServiceNode> referencingServices = serviceNode.getReferences().findRecursiveReferences(ControllerServiceNode.class);
+                for (final ControllerServiceNode referencingService : referencingServices) {
+                    affectedComponents.add(createAffectedComponentEntity(referencingService, user));
+                }
+
+                final List<ProcessorNode> referencingProcessors = serviceNode.getReferences().findRecursiveReferences(ProcessorNode.class);
+                for (final ProcessorNode referencingProcessor : referencingProcessors) {
+                    affectedComponents.add(createAffectedComponentEntity(referencingProcessor, user));
+                }
+            }
+        }
+
+        // Create a map of all connectable components by versioned component ID to the connectable component itself
+        final Map<String, List<Connectable>> connectablesByVersionId = new HashMap<>();
+        mapToConnectableId(group.findAllFunnels(), connectablesByVersionId);
+        mapToConnectableId(group.findAllInputPorts(), connectablesByVersionId);
+        mapToConnectableId(group.findAllOutputPorts(), connectablesByVersionId);
+        mapToConnectableId(group.findAllProcessors(), connectablesByVersionId);
+
+        final List<RemoteGroupPort> remotePorts = new ArrayList<>();
+        for (final RemoteProcessGroup rpg : group.findAllRemoteProcessGroups()) {
+            remotePorts.addAll(rpg.getInputPorts());
+            remotePorts.addAll(rpg.getOutputPorts());
+        }
+        mapToConnectableId(remotePorts, connectablesByVersionId);
+
+        // If any connection is added or modified, we need to stop both the source (if it exists in the flow currently)
+        // and the destination (if it exists in the flow currently).
         for (final FlowDifference difference : comparison.getDifferences()) {
             VersionedComponent component = difference.getComponentA();
             if (component == null) {
                 component = difference.getComponentB();
             }
 
-            if (component.getComponentType() == org.apache.nifi.registry.flow.ComponentType.CONNECTION) {
-                final VersionedConnection connection = (VersionedConnection) component;
+            if (component.getComponentType() != org.apache.nifi.registry.flow.ComponentType.CONNECTION) {
+                continue;
+            }
 
-                final String versionedConnectionId = connection.getIdentifier();
-                final List<Connection> instances = connectionsByVersionedId.get(versionedConnectionId);
-                if (instances == null) {
-                    continue;
+            final VersionedConnection connection = (VersionedConnection) component;
+
+            final String sourceVersionedId = connection.getSource().getId();
+            final List<Connectable> sources = connectablesByVersionId.get(sourceVersionedId);
+            if (sources != null) {
+                for (final Connectable source : sources) {
+                    affectedComponents.add(createAffectedComponentEntity(source, user));
                 }
+            }
 
-                for (final Connection instance : instances) {
-                    affectedComponents.add(createAffectedComponentEntity(instance.getSource(), user));
-                    affectedComponents.add(createAffectedComponentEntity(instance.getDestination(), user));
+            final String destinationVersionId = connection.getDestination().getId();
+            final List<Connectable> destinations = connectablesByVersionId.get(destinationVersionId);
+            if (destinations != null) {
+                for (final Connectable destination : destinations) {
+                    affectedComponents.add(createAffectedComponentEntity(destination, user));
                 }
             }
         }
@@ -4003,6 +4092,18 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return affectedComponents;
     }
 
+    private void mapToConnectableId(final Collection<? extends Connectable> connectables, final Map<String, List<Connectable>> destination) {
+        for (final Connectable connectable : connectables) {
+            final Optional<String> versionedId = connectable.getVersionedComponentId();
+            if (!versionedId.isPresent()) {
+                continue;
+            }
+
+            final List<Connectable> byVersionedId = destination.computeIfAbsent(versionedId.get(), key -> new ArrayList<>());
+            byVersionedId.add(connectable);
+        }
+    }
+
 
     private AffectedComponentEntity createAffectedComponentEntity(final Connectable connectable, final NiFiUser user) {
         final AffectedComponentEntity entity = new AffectedComponentEntity();
@@ -4023,6 +4124,25 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entity;
     }
 
+    private AffectedComponentEntity createAffectedComponentEntity(final ControllerServiceNode serviceNode, final NiFiUser user) {
+        final AffectedComponentEntity entity = new AffectedComponentEntity();
+        entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(serviceNode.getIdentifier())));
+        entity.setId(serviceNode.getIdentifier());
+
+        final Authorizable authorizable = authorizableLookup.getControllerService(serviceNode.getIdentifier()).getAuthorizable();
+        final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable, user);
+        entity.setPermissions(permissionsDto);
+
+        final AffectedComponentDTO dto = new AffectedComponentDTO();
+        dto.setId(serviceNode.getIdentifier());
+        dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        dto.setProcessGroupId(serviceNode.getProcessGroupIdentifier());
+        dto.setState(serviceNode.getState().name());
+
+        entity.setComponent(dto);
+        return entity;
+    }
+
     private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedVersionedComponent instance, final String componentTypeName, final String componentState, final NiFiUser user) {
         final AffectedComponentEntity entity = new AffectedComponentEntity();
         entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(instance.getInstanceId())));

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index b106948..d8c8ddf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -45,6 +45,7 @@ import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.util.BundleUtils;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.ResourceNotFoundException;
+import org.apache.nifi.web.ResumeFlowException;
 import org.apache.nifi.web.Revision;
 import org.apache.nifi.web.api.concurrent.AsyncRequestManager;
 import org.apache.nifi.web.api.concurrent.AsynchronousWebRequest;
@@ -1128,6 +1129,11 @@ public class VersionsResource extends ApplicationResource {
                             idGenerationSeed, true, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
+                    } catch (final ResumeFlowException rfe) {
+                        // Treat ResumeFlowException differently because we don't want to include a message that we couldn't update the flow
+                        // since in this case the flow was successfully updated - we just couldn't re-enable the components.
+                        logger.error(rfe.getMessage(), rfe);
+                        vcur.setFailureReason(rfe.getMessage());
                     } catch (final Exception e) {
                         logger.error("Failed to update flow to new version", e);
                         vcur.setFailureReason("Failed to update flow to new version due to " + e);
@@ -1301,6 +1307,11 @@ public class VersionsResource extends ApplicationResource {
                             idGenerationSeed, false, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
+                    } catch (final ResumeFlowException rfe) {
+                        // Treat ResumeFlowException differently because we don't want to include a message that we couldn't update the flow
+                        // since in this case the flow was successfully updated - we just couldn't re-enable the components.
+                        logger.error(rfe.getMessage(), rfe);
+                        vcur.setFailureReason(rfe.getMessage());
                     } catch (final Exception e) {
                         logger.error("Failed to update flow to new version", e);
                         vcur.setFailureReason("Failed to update flow to new version due to " + e.getMessage());
@@ -1333,13 +1344,15 @@ public class VersionsResource extends ApplicationResource {
     private VersionControlInformationEntity updateFlowVersion(final String groupId, final ComponentLifecycle componentLifecycle, final URI exampleUri,
         final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user, final boolean replicateRequest, final Revision revision, final VersionControlInformationEntity requestEntity,
         final VersionedFlowSnapshot flowSnapshot, final AsynchronousWebRequest<VersionControlInformationEntity> asyncRequest, final String idGenerationSeed,
-        final boolean verifyNotModified, final boolean updateDescendantVersionedFlows) throws LifecycleManagementException {
+        final boolean verifyNotModified, final boolean updateDescendantVersionedFlows) throws LifecycleManagementException, ResumeFlowException {
 
         // Steps 6-7: Determine which components must be stopped and stop them.
         final Set<String> stoppableReferenceTypes = new HashSet<>();
         stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
         stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_REMOTE_INPUT_PORT);
         stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_REMOTE_OUTPUT_PORT);
+        stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_INPUT_PORT);
+        stoppableReferenceTypes.add(AffectedComponentDTO.COMPONENT_TYPE_OUTPUT_PORT);
 
         final Set<AffectedComponentEntity> runningComponents = affectedComponents.stream()
             .filter(dto -> stoppableReferenceTypes.contains(dto.getComponent().getReferenceType()))
@@ -1459,7 +1472,14 @@ public class VersionsResource extends ApplicationResource {
                 asyncRequest.setCancelCallback(enableServicesPause::cancel);
                 final Set<AffectedComponentEntity> servicesToEnable = getUpdatedEntities(enabledServices, user);
                 logger.info("Successfully updated flow; re-enabling {} Controller Services", servicesToEnable.size());
-                componentLifecycle.activateControllerServices(exampleUri, user, groupId, servicesToEnable, ControllerServiceState.ENABLED, enableServicesPause);
+
+                try {
+                    componentLifecycle.activateControllerServices(exampleUri, user, groupId, servicesToEnable, ControllerServiceState.ENABLED, enableServicesPause);
+                } catch (final IllegalStateException ise) {
+                    // Component Lifecycle will re-enable the Controller Services only if they are valid. If IllegalStateException gets thrown, we need to provide
+                    // a more intelligent error message as to exactly what happened, rather than indicate that the flow could not be updated.
+                    throw new ResumeFlowException("Failed to re-enable Controller Services because " + ise.getMessage(), ise);
+                }
             }
 
             if (!asyncRequest.isCancelled()) {
@@ -1474,7 +1494,14 @@ public class VersionsResource extends ApplicationResource {
                 final CancellableTimedPause startComponentsPause = new CancellableTimedPause(250, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
                 asyncRequest.setCancelCallback(startComponentsPause::cancel);
                 logger.info("Restarting {} Processors", componentsToStart.size());
-                componentLifecycle.scheduleComponents(exampleUri, user, groupId, componentsToStart, ScheduledState.RUNNING, startComponentsPause);
+
+                try {
+                    componentLifecycle.scheduleComponents(exampleUri, user, groupId, componentsToStart, ScheduledState.RUNNING, startComponentsPause);
+                } catch (final IllegalStateException ise) {
+                    // Component Lifecycle will restart the Processors only if they are valid. If IllegalStateException gets thrown, we need to provide
+                    // a more intelligent error message as to exactly what happened, rather than indicate that the flow could not be updated.
+                    throw new ResumeFlowException("Failed to restart components because " + ise.getMessage(), ise);
+                }
             }
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 4198303..3d9f521 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -179,6 +179,7 @@ import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
 import org.apache.nifi.web.api.entity.ConnectionStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity;
+import org.apache.nifi.web.api.entity.PortEntity;
 import org.apache.nifi.web.api.entity.PortStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
@@ -1799,6 +1800,32 @@ public final class DtoFactory {
         return component;
     }
 
+    public AffectedComponentEntity createAffectedComponentEntity(final PortEntity portEntity, final String referenceType) {
+        if (portEntity == null) {
+            return null;
+        }
+
+        final AffectedComponentEntity component = new AffectedComponentEntity();
+        component.setBulletins(portEntity.getBulletins());
+        component.setId(portEntity.getId());
+        component.setPermissions(portEntity.getPermissions());
+        component.setPosition(portEntity.getPosition());
+        component.setRevision(portEntity.getRevision());
+        component.setUri(portEntity.getUri());
+
+        final PortDTO portDto = portEntity.getComponent();
+        final AffectedComponentDTO componentDto = new AffectedComponentDTO();
+        componentDto.setId(portDto.getId());
+        componentDto.setName(portDto.getName());
+        componentDto.setProcessGroupId(portDto.getParentGroupId());
+        componentDto.setReferenceType(referenceType);
+        componentDto.setState(portDto.getState());
+        componentDto.setValidationErrors(portDto.getValidationErrors());
+        component.setComponent(componentDto);
+
+        return component;
+    }
+
     public AffectedComponentEntity createAffectedComponentEntity(final ControllerServiceEntity serviceEntity) {
         if (serviceEntity == null) {
             return null;

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index 459acfc..7582420 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -25,6 +25,7 @@ import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 
+import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Future;
@@ -74,11 +75,10 @@ public interface ProcessGroupDAO {
     /**
      * Verifies the specified controller services can be modified
      *
-     * @param groupId the ID of the process group
      * @param state the desired state
      * @param serviceIds the ID's of the controller services
      */
-    void verifyActivateControllerServices(String groupId, ControllerServiceState state, Set<String> serviceIds);
+    void verifyActivateControllerServices(ControllerServiceState state, Collection<String> serviceIds);
 
     /**
      * Schedules the components in the specified process group.
@@ -93,11 +93,10 @@ public interface ProcessGroupDAO {
     /**
      * Enables or disables the controller services in the specified process group
      *
-     * @param groupId the id of the group
      * @param state the desired state
      * @param serviceIds the ID's of the services to enable or disable
      */
-    Future<Void> activateControllerServices(String groupId, ControllerServiceState state, Set<String> serviceIds);
+    Future<Void> activateControllerServices(ControllerServiceState state, Collection<String> serviceIds);
 
     /**
      * Updates the specified process group.

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 5bbb56f..e1d9e69 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -41,8 +41,10 @@ import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.dao.ProcessGroupDAO;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -130,18 +132,18 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
-    public void verifyActivateControllerServices(final String groupId, final ControllerServiceState state, final Set<String> serviceIds) {
-        final ProcessGroup group = locateProcessGroup(flowController, groupId);
-
-        group.findAllControllerServices().stream()
-            .filter(service -> serviceIds.contains(service.getIdentifier()))
-            .forEach(service -> {
-                if (state == ControllerServiceState.ENABLED) {
-                    service.verifyCanEnable();
-                } else {
-                    service.verifyCanDisable();
-                }
-            });
+    public void verifyActivateControllerServices(final ControllerServiceState state, final Collection<String> serviceIds) {
+        final Set<ControllerServiceNode> serviceNodes = serviceIds.stream()
+            .map(flowController::getControllerServiceNode)
+            .collect(Collectors.toSet());
+
+        for (final ControllerServiceNode serviceNode : serviceNodes) {
+            if (state == ControllerServiceState.ENABLED) {
+                serviceNode.verifyCanEnable(serviceNodes);
+            } else {
+                serviceNode.verifyCanDisable(serviceNodes);
+            }
+        }
     }
 
     @Override
@@ -201,26 +203,16 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
-    public Future<Void> activateControllerServices(final String groupId, final ControllerServiceState state, final Set<String> serviceIds) {
-        final ProcessGroup group = locateProcessGroup(flowController, groupId);
-
-        CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
-        for (final String serviceId : serviceIds) {
-            final ControllerServiceNode serviceNode = group.findControllerService(serviceId, true, true);
-            if (serviceNode == null) {
-                throw new ResourceNotFoundException("Could not find Controller Service with identifier " + serviceId);
-            }
-
-            if (ControllerServiceState.ENABLED.equals(state)) {
-                final CompletableFuture<Void> serviceFuture = flowController.enableControllerService(serviceNode);
-                future = CompletableFuture.allOf(future, serviceFuture);
-            } else {
-                final CompletableFuture<Void> serviceFuture = flowController.disableControllerService(serviceNode);
-                future = CompletableFuture.allOf(future, serviceFuture);
-            }
+    public Future<Void> activateControllerServices(final ControllerServiceState state, final Collection<String> serviceIds) {
+        final List<ControllerServiceNode> serviceNodes = serviceIds.stream()
+            .map(flowController::getControllerServiceNode)
+            .collect(Collectors.toList());
+
+        if (state == ControllerServiceState.ENABLED) {
+            return flowController.enableControllerServicesAsync(serviceNodes);
+        } else {
+            return flowController.disableControllerServicesAsync(serviceNodes);
         }
-
-        return future;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
index a801dcb..f257bb1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/AffectedComponentUtils.java
@@ -27,6 +27,7 @@ import org.apache.nifi.web.api.dto.RemoteProcessGroupContentsDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.api.entity.PortEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 
@@ -39,6 +40,14 @@ public class AffectedComponentUtils {
             case AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR:
                 final ProcessorEntity procEntity = serviceFacade.getProcessor(componentEntity.getId(), user);
                 return dtoFactory.createAffectedComponentEntity(procEntity);
+            case AffectedComponentDTO.COMPONENT_TYPE_INPUT_PORT: {
+                final PortEntity portEntity = serviceFacade.getInputPort(componentEntity.getId(), user);
+                return dtoFactory.createAffectedComponentEntity(portEntity, AffectedComponentDTO.COMPONENT_TYPE_INPUT_PORT);
+            }
+            case AffectedComponentDTO.COMPONENT_TYPE_OUTPUT_PORT: {
+                final PortEntity portEntity = serviceFacade.getOutputPort(componentEntity.getId(), user);
+                return dtoFactory.createAffectedComponentEntity(portEntity, AffectedComponentDTO.COMPONENT_TYPE_OUTPUT_PORT);
+            }
             case AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE:
                 final ControllerServiceEntity serviceEntity = serviceFacade.getControllerService(componentEntity.getId(), user);
                 return dtoFactory.createAffectedComponentEntity(serviceEntity);

http://git-wip-us.apache.org/repos/asf/nifi/blob/b6117743/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
index e005d28..1c7e82d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
@@ -18,7 +18,9 @@
 package org.apache.nifi.web.util;
 
 import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.Revision;
@@ -33,6 +35,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -228,6 +233,46 @@ public class LocalComponentLifecycle implements ComponentLifecycle {
         waitForControllerServiceState(processGroupId, affectedServices, ControllerServiceState.DISABLED, pause, user);
     }
 
+    static List<List<ControllerServiceNode>> determineEnablingOrder(final Map<String, ControllerServiceNode> serviceNodeMap) {
+        final List<List<ControllerServiceNode>> orderedNodeLists = new ArrayList<>();
+
+        for (final ControllerServiceNode node : serviceNodeMap.values()) {
+            final List<ControllerServiceNode> branch = new ArrayList<>();
+            determineEnablingOrder(serviceNodeMap, node, branch, new HashSet<ControllerServiceNode>());
+            orderedNodeLists.add(branch);
+        }
+
+        return orderedNodeLists;
+    }
+
+    private static void determineEnablingOrder(
+        final Map<String, ControllerServiceNode> serviceNodeMap,
+        final ControllerServiceNode contextNode,
+        final List<ControllerServiceNode> orderedNodes,
+        final Set<ControllerServiceNode> visited) {
+        if (visited.contains(contextNode)) {
+            return;
+        }
+
+        for (final Map.Entry<PropertyDescriptor, String> entry : contextNode.getProperties().entrySet()) {
+            if (entry.getKey().getControllerServiceDefinition() != null) {
+                final String referencedServiceId = entry.getValue();
+                if (referencedServiceId != null) {
+                    final ControllerServiceNode referencedNode = serviceNodeMap.get(referencedServiceId);
+                    if (!orderedNodes.contains(referencedNode)) {
+                        visited.add(contextNode);
+                        determineEnablingOrder(serviceNodeMap, referencedNode, orderedNodes, visited);
+                    }
+                }
+            }
+        }
+
+        if (!orderedNodes.contains(contextNode)) {
+            orderedNodes.add(contextNode);
+        }
+    }
+
+
     /**
      * Periodically polls the process group with the given ID, waiting for all controller services whose ID's are given to have the given Controller Service State.
      *


[41/50] nifi git commit: NIFI-4436: - Updating buckets permissions based on new model. - Adding check to ensure that flow name is non null before checking the length. - Adding versioned flow state to the Process Group tab in the Summary table. - Fixing i

Posted by bb...@apache.org.
NIFI-4436:
- Updating buckets permissions based on new model.
- Adding check to ensure that flow name is non null before checking the length.
- Adding versioned flow state to the Process Group tab in the Summary table.
- Fixing issue with navigating to Controller Services from the local changes dialog.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/f48808b1
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/f48808b1
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/f48808b1

Branch: refs/heads/master
Commit: f48808b1f4b817203aa5fec92b2a90bfeefb59e9
Parents: 181d680
Author: Matt Gilman <ma...@gmail.com>
Authored: Tue Dec 12 16:39:05 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:56 2018 -0500

----------------------------------------------------------------------
 .../controller/status/ProcessGroupStatus.java   | 16 +++++
 .../nifi/registry/flow/VersionedFlowState.java  | 63 ++++++++++++++++++
 .../status/ProcessGroupStatusSnapshotDTO.java   | 14 ++++
 .../web/api/entity/FlowBreadcrumbEntity.java    | 10 +--
 .../nifi/web/api/entity/ProcessGroupEntity.java | 10 +--
 .../nifi/cluster/manager/StatusMerger.java      |  6 ++
 .../nifi/registry/flow/VersionedFlowState.java  | 63 ------------------
 .../apache/nifi/controller/FlowController.java  | 70 +++++++++++---------
 .../nifi/web/StandardNiFiServiceFacade.java     |  7 +-
 .../apache/nifi/web/api/VersionsResource.java   |  2 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  9 ++-
 .../apache/nifi/web/api/dto/EntityFactory.java  |  4 +-
 .../controllers/nf-ng-breadcrumbs-controller.js |  8 +--
 .../main/webapp/js/nf/canvas/nf-flow-version.js |  5 +-
 .../webapp/js/nf/canvas/nf-process-group.js     |  6 +-
 .../webapp/js/nf/summary/nf-summary-table.js    | 55 ++++++++++++---
 16 files changed, 215 insertions(+), 133 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-api/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java
----------------------------------------------------------------------
diff --git a/nifi-api/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java b/nifi-api/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java
index b890c85..e07d1c1 100644
--- a/nifi-api/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java
+++ b/nifi-api/src/main/java/org/apache/nifi/controller/status/ProcessGroupStatus.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.controller.status;
 
+import org.apache.nifi.registry.flow.VersionedFlowState;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -27,6 +29,7 @@ public class ProcessGroupStatus implements Cloneable {
 
     private String id;
     private String name;
+    private VersionedFlowState versionedFlowState;
     private Integer inputCount;
     private Long inputContentSize;
     private Integer outputCount;
@@ -66,6 +69,14 @@ public class ProcessGroupStatus implements Cloneable {
         this.name = name;
     }
 
+    public VersionedFlowState getVersionedFlowState() {
+        return versionedFlowState;
+    }
+
+    public void setVersionedFlowState(VersionedFlowState versionedFlowState) {
+        this.versionedFlowState = versionedFlowState;
+    }
+
     public Integer getInputCount() {
         return inputCount;
     }
@@ -399,6 +410,11 @@ public class ProcessGroupStatus implements Cloneable {
         target.setFlowFilesSent(target.getFlowFilesSent() + toMerge.getFlowFilesSent());
         target.setBytesSent(target.getBytesSent() + toMerge.getBytesSent());
 
+        // if the versioned flow state to merge is sync failure allow it to take precedence.
+        if (VersionedFlowState.SYNC_FAILURE.equals(toMerge.getVersionedFlowState())) {
+            target.setVersionedFlowState(VersionedFlowState.SYNC_FAILURE);
+        }
+
         // connection status
         // sort by id
         final Map<String, ConnectionStatus> mergedConnectionMap = new HashMap<>();

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
----------------------------------------------------------------------
diff --git a/nifi-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java b/nifi-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
new file mode 100644
index 0000000..35b436d
--- /dev/null
+++ b/nifi-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
@@ -0,0 +1,63 @@
+/*
+ * 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.registry.flow;
+
+public enum VersionedFlowState {
+
+    /**
+     * We are unable to communicate with the Flow Registry in order to determine the appropriate state
+     */
+    SYNC_FAILURE("Failed to communicate with Flow Registry"),
+
+    /**
+     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
+     * is on the latest version of the Versioned Flow, but is different than the Versioned Flow that is
+     * stored in the Flow Registry.
+     */
+    LOCALLY_MODIFIED("Local changes have been made"),
+
+    /**
+     * This Process Group has not been modified since it was last synchronized with the Flow Registry, but
+     * the Flow Registry has a newer version of the flow than what is contained in this Process Group.
+     */
+    STALE("A newer version of this flow is available"),
+
+    /**
+     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
+     * has been modified since it was last synchronized with the Flow Registry, and the Flow Registry has
+     * a newer version of the flow than what is contained in this Process Group.
+     */
+    LOCALLY_MODIFIED_AND_STALE("Local changes have been made and a newer version of this flow is available"),
+
+    /**
+     * This Process Group and all child/descendant Process Groups are on the latest version of the flow in
+     * the Flow Registry and have no local modifications.
+     */
+    UP_TO_DATE("Flow version is current");
+
+
+    private final String description;
+
+    private VersionedFlowState(final String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessGroupStatusSnapshotDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessGroupStatusSnapshotDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessGroupStatusSnapshotDTO.java
index 3ae6355..82df92e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessGroupStatusSnapshotDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/status/ProcessGroupStatusSnapshotDTO.java
@@ -43,6 +43,8 @@ public class ProcessGroupStatusSnapshotDTO implements Cloneable {
     private Collection<PortStatusSnapshotEntity> inputPortStatusSnapshots;
     private Collection<PortStatusSnapshotEntity> outputPortStatusSnapshots;
 
+    private String versionedFlowState;
+
     private Integer flowFilesIn = 0;
     private Long bytesIn = 0L;
     private String input;
@@ -102,6 +104,17 @@ public class ProcessGroupStatusSnapshotDTO implements Cloneable {
         this.name = name;
     }
 
+    @ApiModelProperty(readOnly = true,
+            value = "The current state of the Process Group, as it relates to the Versioned Flow",
+            allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")
+    public String getVersionedFlowState() {
+        return versionedFlowState;
+    }
+
+    public void setVersionedFlowState(String versionedFlowState) {
+        this.versionedFlowState = versionedFlowState;
+    }
+
     /**
      * @return active thread count for this process group
      */
@@ -477,6 +490,7 @@ public class ProcessGroupStatusSnapshotDTO implements Cloneable {
         final ProcessGroupStatusSnapshotDTO other = new ProcessGroupStatusSnapshotDTO();
         other.setId(getId());
         other.setName(getName());
+        other.setVersionedFlowState(getVersionedFlowState());
 
         other.setBytesIn(getBytesIn());
         other.setFlowFilesIn(getFlowFilesIn());

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
index 21f9742..83934d2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/FlowBreadcrumbEntity.java
@@ -30,7 +30,7 @@ public class FlowBreadcrumbEntity extends Entity {
 
     private String id;
     private PermissionsDTO permissions;
-    private String state;
+    private String versionedFlowState;
     private FlowBreadcrumbDTO breadcrumb;
     private FlowBreadcrumbEntity parentBreadcrumb;
 
@@ -101,11 +101,11 @@ public class FlowBreadcrumbEntity extends Entity {
     @ApiModelProperty(readOnly = true,
             value = "The current state of the Process Group, as it relates to the Versioned Flow",
             allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")
-    public String getState() {
-        return state;
+    public String getVersionedFlowState() {
+        return versionedFlowState;
     }
 
-    public void setState(String state) {
-        this.state = state;
+    public void setVersionedFlowState(String versionedFlowState) {
+        this.versionedFlowState = versionedFlowState;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
index fe8d2d6..9cb7de6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ProcessGroupEntity.java
@@ -40,7 +40,7 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
     private Integer activeRemotePortCount;
     private Integer inactiveRemotePortCount;
 
-    private String state;
+    private String versionedFlowState;
 
     private Integer upToDateCount;
     private Integer locallyModifiedCount;
@@ -204,12 +204,12 @@ public class ProcessGroupEntity extends ComponentEntity implements Permissible<P
     @ApiModelProperty(readOnly = true,
             value = "The current state of the Process Group, as it relates to the Versioned Flow",
             allowableValues = "LOCALLY_MODIFIED_DESCENDANT, LOCALLY_MODIFIED, STALE, LOCALLY_MODIFIED_AND_STALE, UP_TO_DATE")
-    public String getState() {
-        return state;
+    public String getVersionedFlowState() {
+        return versionedFlowState;
     }
 
-    public void setState(String state) {
-        this.state = state;
+    public void setVersionedFlowState(String versionedFlowState) {
+        this.versionedFlowState = versionedFlowState;
     }
 
     @ApiModelProperty("The number of up to date versioned process groups in the process group.")

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
index f6b124d..9c940b4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/StatusMerger.java
@@ -19,6 +19,7 @@ package org.apache.nifi.cluster.manager;
 
 import org.apache.nifi.controller.status.RunStatus;
 import org.apache.nifi.controller.status.TransmissionStatus;
+import org.apache.nifi.registry.flow.VersionedFlowState;
 import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.web.api.dto.CounterDTO;
 import org.apache.nifi.web.api.dto.CountersDTO;
@@ -122,6 +123,11 @@ public class StatusMerger {
             target.setName(toMerge.getName());
         }
 
+        // if the versioned flow state to merge is sync failure allow it to take precedence
+        if (VersionedFlowState.SYNC_FAILURE.name().equals(toMerge.getVersionedFlowState())) {
+            target.setVersionedFlowState(VersionedFlowState.SYNC_FAILURE.name());
+        }
+
         target.setBytesIn(target.getBytesIn() + toMerge.getBytesIn());
         target.setFlowFilesIn(target.getFlowFilesIn() + toMerge.getFlowFilesIn());
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
deleted file mode 100644
index 35b436d..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/VersionedFlowState.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.registry.flow;
-
-public enum VersionedFlowState {
-
-    /**
-     * We are unable to communicate with the Flow Registry in order to determine the appropriate state
-     */
-    SYNC_FAILURE("Failed to communicate with Flow Registry"),
-
-    /**
-     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
-     * is on the latest version of the Versioned Flow, but is different than the Versioned Flow that is
-     * stored in the Flow Registry.
-     */
-    LOCALLY_MODIFIED("Local changes have been made"),
-
-    /**
-     * This Process Group has not been modified since it was last synchronized with the Flow Registry, but
-     * the Flow Registry has a newer version of the flow than what is contained in this Process Group.
-     */
-    STALE("A newer version of this flow is available"),
-
-    /**
-     * This Process Group (or a child/descendant Process Group that is not itself under Version Control)
-     * has been modified since it was last synchronized with the Flow Registry, and the Flow Registry has
-     * a newer version of the flow than what is contained in this Process Group.
-     */
-    LOCALLY_MODIFIED_AND_STALE("Local changes have been made and a newer version of this flow is available"),
-
-    /**
-     * This Process Group and all child/descendant Process Groups are on the latest version of the flow in
-     * the Flow Registry and have no local modifications.
-     */
-    UP_TO_DATE("Flow version is current");
-
-
-    private final String description;
-
-    private VersionedFlowState(final String description) {
-        this.description = description;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index f7c2545..eb4b8b9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -16,39 +16,6 @@
  */
 package org.apache.nifi.controller;
 
-import static java.util.Objects.requireNonNull;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
-
-import javax.net.ssl.SSLContext;
-
 import org.apache.commons.collections4.Predicate;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -258,6 +225,38 @@ import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+
 public class FlowController implements EventAccess, ControllerServiceProvider, ReportingTaskProvider,
     QueueProvider, Authorizable, ProvenanceAuthorizableFactory, NodeTypeProvider, IdentifierLookup, ReloadComponent {
 
@@ -3005,6 +3004,11 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
         status.setFlowFilesTransferred(flowFilesTransferred);
         status.setBytesTransferred(bytesTransferred);
 
+        final VersionControlInformation vci = group.getVersionControlInformation();
+        if (vci != null && vci.getStatus() != null && vci.getStatus().getState() != null) {
+            status.setVersionedFlowState(vci.getStatus().getState());
+        }
+
         return status;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 11bc39d..1da63c2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -115,6 +115,7 @@ import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
+import org.apache.nifi.registry.model.authorization.Permissions;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.reporting.Bulletin;
@@ -2417,10 +2418,10 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
                     dto.setDescription(bucket.getDescription());
                     dto.setCreated(bucket.getCreatedTimestamp());
 
-                    final Set<String> authorizedActions = bucket.getAuthorizedActions();
+                    final Permissions regPermissions = bucket.getPermissions();
                     final PermissionsDTO permissions = new PermissionsDTO();
-                    permissions.setCanRead(authorizedActions.contains("read"));
-                    permissions.setCanWrite(authorizedActions.contains("write"));
+                    permissions.setCanRead(regPermissions.getCanRead());
+                    permissions.setCanWrite(regPermissions.getCanWrite());
 
                     return entityFactory.createBucketEntity(dto, permissions);
                 })

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 1d4cd88..af9a515 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -437,7 +437,7 @@ public class VersionsResource extends ApplicationResource {
         if (StringUtils.isEmpty(versionedFlowDto.getFlowName()) && StringUtils.isEmpty(versionedFlowDto.getFlowId())) {
             throw new IllegalArgumentException("The Flow Name or Flow ID must be supplied.");
         }
-        if (versionedFlowDto.getFlowName().length() > 1000) {
+        if (versionedFlowDto.getFlowName() != null && versionedFlowDto.getFlowName().length() > 1000) {
             throw new IllegalArgumentException("The Flow Name cannot exceed 1,000 characters");
         }
         if (StringUtils.isEmpty(versionedFlowDto.getRegistryId())) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 5bdb040..4198303 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,8 +16,6 @@
  */
 package org.apache.nifi.web.api.dto;
 
-import javax.ws.rs.WebApplicationException;
-
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -192,6 +190,7 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
+import javax.ws.rs.WebApplicationException;
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -955,6 +954,10 @@ public final class DtoFactory {
         snapshot.setId(processGroupStatus.getId());
         snapshot.setName(processGroupStatus.getName());
 
+        if (processGroupStatus.getVersionedFlowState() != null) {
+            snapshot.setVersionedFlowState(processGroupStatus.getVersionedFlowState().name());
+        }
+
         snapshot.setFlowFilesQueued(processGroupStatus.getQueuedCount());
         snapshot.setBytesQueued(processGroupStatus.getQueuedContentSize());
         snapshot.setBytesRead(processGroupStatus.getBytesRead());
@@ -2214,7 +2217,7 @@ public final class DtoFactory {
 
         final ComponentDifferenceDTO dto = new ComponentDifferenceDTO();
         dto.setComponentName(component.getName());
-        dto.setComponentType(component.getComponentType().name());
+        dto.setComponentType(component.getComponentType().toString());
 
         if (component instanceof InstantiatedVersionedComponent) {
             final InstantiatedVersionedComponent instantiatedComponent = (InstantiatedVersionedComponent) component;

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index 6a73e3a..566b519 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -241,7 +241,7 @@ public final class EntityFactory {
             entity.setSyncFailureCount(dto.getSyncFailureCount());
 
             if (dto.getVersionControlInformation() != null) {
-                entity.setState(dto.getVersionControlInformation().getState());
+                entity.setVersionedFlowState(dto.getVersionControlInformation().getState());
             }
 
             entity.setBulletins(bulletins); // include bulletins as authorized descendant component bulletins should be available
@@ -513,7 +513,7 @@ public final class EntityFactory {
             entity.setId(dto.getId());
 
             if (dto.getVersionControlInformation() != null) {
-                entity.setState(dto.getVersionControlInformation().getState());
+                entity.setVersionedFlowState(dto.getVersionControlInformation().getState());
             }
 
             if (permissions != null && permissions.getCanRead()) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
index 64f2117..7331596 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
@@ -103,7 +103,7 @@
              * @returns {*}
              */
             isTracking: function (breadcrumbEntity) {
-                return nfCommon.isDefinedAndNotNull(breadcrumbEntity.state);
+                return nfCommon.isDefinedAndNotNull(breadcrumbEntity.versionedFlowState);
             },
 
             /**
@@ -113,8 +113,8 @@
              * @returns {string}
              */
             getVersionControlClass: function (breadcrumbEntity) {
-                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.state)) {
-                    var vciState = breadcrumbEntity.state;
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.versionedFlowState)) {
+                    var vciState = breadcrumbEntity.versionedFlowState;
                     if (vciState === 'SYNC_FAILURE') {
                         return 'breadcrumb-version-control-gray fa fa-question'
                     } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
@@ -137,7 +137,7 @@
              * @param breadcrumbEntity
              */
             getVersionControlTooltip: function (breadcrumbEntity) {
-                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.state) && breadcrumbEntity.permissions.canRead) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.versionedFlowState) && breadcrumbEntity.permissions.canRead) {
                     return nfCommon.getVersionControlTooltip(breadcrumbEntity.breadcrumb.versionControlInformation);
                 } else {
                     return 'This Process Group is not under version control.'

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 01a0a07..b676f5b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -1602,10 +1602,9 @@
                             var processGroupId = $('#save-flow-version-process-group-id').text();
                             saveFlowVersion().done(function (response) {
                                 updateVersionControlInformation(processGroupId, response.versionControlInformation);
-
-                                // only hide the dialog if the flow version was successfully saved
-                                $(this).modal('hide');
                             });
+
+                            $(this).modal('hide');
                         }
                     }
                 }, {

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 433c59f..50a3818 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -91,7 +91,7 @@
      * @param d
      */
     var isUnderVersionControl = function (d) {
-        return nfCommon.isDefinedAndNotNull(d.state);
+        return nfCommon.isDefinedAndNotNull(d.versionedFlowState);
     };
 
     /**
@@ -1060,7 +1060,7 @@
                         'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
                         'fill': function () {
                             if (isUnderVersionControl(processGroupData)) {
-                                var vciState = processGroupData.state;
+                                var vciState = processGroupData.versionedFlowState;
                                 if (vciState === 'SYNC_FAILURE') {
                                     return '#666666';
                                 } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
@@ -1079,7 +1079,7 @@
                     })
                     .text(function () {
                         if (isUnderVersionControl(processGroupData)) {
-                            var vciState = processGroupData.state;
+                            var vciState = processGroupData.versionedFlowState;
                             if (vciState === 'SYNC_FAILURE') {
                                 return '\uf128'
                             } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {

http://git-wip-us.apache.org/repos/asf/nifi/blob/f48808b1/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
index dbc3b70..e54b441 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/summary/nf-summary-table.js
@@ -310,25 +310,25 @@
             if (nfCommon.isDefinedAndNotNull(dataContext.activeThreadCount) && dataContext.activeThreadCount > 0) {
                 activeThreadCount = '(' + nfCommon.escapeHtml(dataContext.activeThreadCount) + ')';
             }
-            var classes = nfCommon.escapeHtml(value.toLowerCase());
-            switch (nfCommon.escapeHtml(value.toLowerCase())) {
+            var classes;
+            switch (value.toLowerCase()) {
                 case 'running':
-                    classes += ' fa fa-play running';
+                    classes = 'fa fa-play running';
                     break;
                 case 'stopped':
-                    classes += ' fa fa-stop stopped';
+                    classes = 'fa fa-stop stopped';
                     break;
                 case 'enabled':
-                    classes += ' fa fa-flash enabled';
+                    classes = 'fa fa-flash enabled';
                     break;
                 case 'disabled':
-                    classes += ' icon icon-enable-false disabled';
+                    classes = 'icon icon-enable-false disabled';
                     break;
                 case 'invalid':
-                    classes += ' fa fa-warning invalid';
+                    classes = 'fa fa-warning invalid';
                     break;
                 default:
-                    classes += '';
+                    classes = '';
             }
             var formattedValue = '<div layout="row"><div class="' + classes + '"></div>';
             return formattedValue + '<div class="status-text" style="margin-top: 4px;">' + nfCommon.escapeHtml(value) + '</div><div style="float: left; margin-left: 4px;">' + nfCommon.escapeHtml(activeThreadCount) + '</div></div>';
@@ -1041,6 +1041,37 @@
             formatter: nfCommon.genericValueFormatter
         };
 
+        // define how the column is formatted
+        var versionStateFormatter = function (row, cell, value, columnDef, dataContext) {
+            var classes, label;
+            switch (value) {
+                case 'UP_TO_DATE':
+                    classes = 'fa fa-check up-to-date';
+                    label = 'Up to date';
+                    break;
+                case 'LOCALLY_MODIFIED':
+                    classes = 'fa fa-asterisk locally-modified';
+                    label = 'Locally modified';
+                    break;
+                case 'STALE':
+                    classes = 'fa fa-arrow-circle-up stale';
+                    label = 'Stale';
+                    break;
+                case 'LOCALLY_MODIFIED_AND_STALE':
+                    classes = 'fa fa-exclamation-circle locally-modified-and-stale';
+                    label = 'Locally modified and stale';
+                    break;
+                case 'SYNC_FAILURE':
+                    classes = 'fa fa-question sync-failure';
+                    label = 'Sync failure';
+                    break;
+                default:
+                    classes = '';
+                    label = '';
+            }
+            return '<div layout="row"><div class="' + classes + '"></div><div class="status-text" style="margin-top: 4px;">' + label + '</div></div>';
+        };
+
         // define the column model for the summary table
         var processGroupsColumnModel = [
             moreDetailsColumn,
@@ -1052,6 +1083,14 @@
                 resizable: true,
                 formatter: valueFormatter
             },
+            {
+                id: 'versionedFlowState',
+                field: 'versionedFlowState',
+                name: 'Version State',
+                sortable: true,
+                resizable: true,
+                formatter: versionStateFormatter
+            },
             transferredColumn,
             inputColumn,
             ioColumn,


[31/50] nifi git commit: NIFI-4436, NIFI-4461: When copying and pasting an RPG, ensure that we copy Batch Settings for each Port. Bug fixes. Now works in clustered mode.

Posted by bb...@apache.org.
NIFI-4436, NIFI-4461: When copying and pasting an RPG, ensure that we copy Batch Settings for each Port. Bug fixes. Now works in clustered mode.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/e1606701
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/e1606701
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/e1606701

Branch: refs/heads/master
Commit: e1606701c78984f794f96814d8f84a3f686099cf
Parents: c92022d
Author: Mark Payne <ma...@hotmail.com>
Authored: Wed Nov 22 09:55:30 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:54 2018 -0500

----------------------------------------------------------------------
 .../nifi/groups/StandardProcessGroup.java       | 128 +++++++++----------
 .../registry/flow/RestBasedFlowRegistry.java    |   1 +
 .../flow/mapping/NiFiRegistryDtoMapper.java     |   5 +-
 .../flow/mapping/NiFiRegistryFlowMapper.java    |  26 +++-
 .../mapping/StandardComparableDataFlow.java     |  42 ++++++
 .../nifi/remote/StandardRemoteProcessGroup.java |   1 +
 .../fingerprint/FingerprintFactoryTest.java     |   1 +
 .../nifi/web/StandardNiFiServiceFacade.java     |  27 +---
 .../org/apache/nifi/web/api/FlowResource.java   |  18 ---
 .../nifi/web/api/ProcessGroupResource.java      |   2 +-
 .../apache/nifi/web/api/VersionsResource.java   |   6 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |   1 +
 12 files changed, 143 insertions(+), 115 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 51839d0..77be3fe 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull;
 import java.io.IOException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -93,12 +94,15 @@ import org.apache.nifi.processor.StandardProcessContext;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableDescriptor;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.flow.BatchSize;
 import org.apache.nifi.registry.flow.Bundle;
+import org.apache.nifi.registry.flow.ComponentType;
 import org.apache.nifi.registry.flow.ConnectableComponent;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.StandardVersionControlInformation;
 import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedControllerService;
 import org.apache.nifi.registry.flow.VersionedFlow;
@@ -128,7 +132,6 @@ import org.apache.nifi.remote.StandardRemoteProcessGroupPortDescriptor;
 import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
 import org.apache.nifi.scheduling.ExecutionNode;
 import org.apache.nifi.scheduling.SchedulingStrategy;
-import org.apache.nifi.util.ComponentIdGenerator;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.ReflectionUtils;
 import org.apache.nifi.web.Revision;
@@ -145,6 +148,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     private final AtomicReference<String> comments;
     private final AtomicReference<String> versionedComponentId = new AtomicReference<>();
     private final AtomicReference<StandardVersionControlInformation> versionControlInfo = new AtomicReference<>();
+    private static final SecureRandom randomGenerator = new SecureRandom();
 
     private final StandardProcessScheduler scheduler;
     private final ControllerServiceProvider controllerServiceProvider;
@@ -3018,10 +3022,20 @@ public final class StandardProcessGroup implements ProcessGroup {
             final FlowComparator flowComparator = new StandardFlowComparator(localFlow, remoteFlow, new StaticDifferenceDescriptor());
             final FlowComparison flowComparison = flowComparator.compare();
 
-            final Set<String> updatedVersionedComponentIds = flowComparison.getDifferences().stream()
-                .filter(diff -> diff.getDifferenceType() != DifferenceType.POSITION_CHANGED)
-                .map(diff -> diff.getComponentA() == null ? diff.getComponentB().getIdentifier() : diff.getComponentA().getIdentifier())
-                .collect(Collectors.toSet());
+            final Set<String> updatedVersionedComponentIds = new HashSet<>();
+            for (final FlowDifference diff : flowComparison.getDifferences()) {
+                if (diff.getDifferenceType() == DifferenceType.POSITION_CHANGED) {
+                    continue;
+                }
+
+                final VersionedComponent component = diff.getComponentA() == null ? diff.getComponentB() : diff.getComponentA();
+                updatedVersionedComponentIds.add(component.getIdentifier());
+
+                if (component.getComponentType() == ComponentType.REMOTE_INPUT_PORT || component.getComponentType() == ComponentType.REMOTE_OUTPUT_PORT) {
+                    final String remoteGroupId = ((VersionedRemoteGroupPort) component).getRemoteGroupId();
+                    updatedVersionedComponentIds.add(remoteGroupId);
+                }
+            }
 
             if (LOG.isInfoEnabled()) {
                 final String differencesByLine = flowComparison.getDifferences().stream()
@@ -3106,13 +3120,13 @@ public final class StandardProcessGroup implements ProcessGroup {
                 .registryId(registryId)
                 .registryName(registryName)
                 .bucketId(bucketId)
-                .bucketName(bucketId) // bucket name not yet known
+                .bucketName(bucketId)
                 .flowId(flowId)
-                .flowName(flowId) // flow id not yet known
+                .flowName(flowId)
                 .version(version)
                 .flowSnapshot(proposed)
                 .modified(false)
-                .current(true)
+                .current(remoteCoordinates.getLatest())
                 .build();
 
             group.setVersionControlInformation(vci, Collections.emptyMap());
@@ -3125,7 +3139,6 @@ public final class StandardProcessGroup implements ProcessGroup {
 
         for (final VersionedProcessGroup proposedChildGroup : proposed.getProcessGroups()) {
             final ProcessGroup childGroup = childGroupsByVersionedId.get(proposedChildGroup.getIdentifier());
-
             final VersionedFlowCoordinates childCoordinates = proposedChildGroup.getVersionedFlowCoordinates();
 
             if (childGroup == null) {
@@ -3291,7 +3304,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 final RemoteProcessGroup added = addRemoteProcessGroup(group, proposedRpg, componentIdSeed);
                 LOG.info("Added {} to {}", added, this);
             } else if (updatedVersionedComponentIds.contains(proposedRpg.getIdentifier())) {
-                updateRemoteProcessGroup(rpg, proposedRpg);
+                updateRemoteProcessGroup(rpg, proposedRpg, componentIdSeed);
                 LOG.info("Updated {}", rpg);
             } else {
                 rpg.setPosition(new Position(proposedRpg.getPosition().getX(), proposedRpg.getPosition().getY()));
@@ -3388,27 +3401,28 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
-    protected String generateUuid(final String componentIdSeed) {
+    private String generateUuid(final String propposedId, final String destinationGroupId, final String seed) {
+        // TODO: I think we can get rid of all of those LinkedHashSet's now in the VersionedProcessGroup because
+        /// the UUID is properly keyed off of the ID of the component in the VersionedProcessGroup.
+        long msb = UUID.nameUUIDFromBytes((propposedId + destinationGroupId).getBytes(StandardCharsets.UTF_8)).getMostSignificantBits();
+
         UUID uuid;
-        if (componentIdSeed == null) {
-            uuid = ComponentIdGenerator.generateId();
+        if (StringUtils.isBlank(seed)) {
+            long lsb = randomGenerator.nextLong();
+            // since msb is extracted from type-one UUID, the type-one semantics will be preserved
+            uuid = new UUID(msb, lsb);
         } else {
-            try {
-                UUID seedId = UUID.fromString(componentIdSeed);
-                uuid = new UUID(seedId.getMostSignificantBits(), componentIdSeed.hashCode());
-            } catch (Exception e) {
-                LOG.warn("Provided 'seed' does not represent UUID. Will not be able to extract most significant bits for ID generation.");
-                uuid = UUID.nameUUIDFromBytes(componentIdSeed.getBytes(StandardCharsets.UTF_8));
-            }
+            UUID seedId = UUID.nameUUIDFromBytes((propposedId + destinationGroupId + seed).getBytes(StandardCharsets.UTF_8));
+            uuid = new UUID(msb, seedId.getLeastSignificantBits());
         }
-
+        LOG.debug("Generating UUID {} from currentId={}, seed={}", uuid, propposedId, seed);
         return uuid.toString();
     }
 
 
     private ProcessGroup addProcessGroup(final ProcessGroup destination, final VersionedProcessGroup proposed, final String componentIdSeed, final Set<String> variablesToSkip)
             throws ProcessorInstantiationException {
-        final ProcessGroup group = flowController.createProcessGroup(generateUuid(componentIdSeed));
+        final ProcessGroup group = flowController.createProcessGroup(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed));
         group.setVersionedComponentId(proposed.getIdentifier());
         group.setParent(destination);
         updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true, true, variablesToSkip);
@@ -3461,7 +3475,8 @@ public final class StandardProcessGroup implements ProcessGroup {
                 + " but no component could be found in the Process Group with a corresponding identifier");
         }
 
-        final Connection connection = flowController.createConnection(generateUuid(componentIdSeed), proposed.getName(), source, destination, proposed.getSelectedRelationships());
+        final Connection connection = flowController.createConnection(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), proposed.getName(), source, destination,
+            proposed.getSelectedRelationships());
         connection.setVersionedComponentId(proposed.getIdentifier());
         destinationGroup.addConnection(connection);
         updateConnection(connection, proposed);
@@ -3523,7 +3538,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 final String rpgId = connectableComponent.getGroupId();
                 final Optional<RemoteProcessGroup> rpgOption = group.getRemoteProcessGroups().stream()
                     .filter(component -> component.getVersionedComponentId().isPresent())
-                    .filter(component -> id.equals(component.getVersionedComponentId().get()))
+                    .filter(component -> rpgId.equals(component.getVersionedComponentId().get()))
                     .findAny();
 
                 if (!rpgOption.isPresent()) {
@@ -3598,7 +3613,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
     private ControllerServiceNode addControllerService(final ProcessGroup destination, final VersionedControllerService proposed, final String componentIdSeed) {
         final String type = proposed.getType();
-        final String id = generateUuid(componentIdSeed);
+        final String id = generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed);
 
         final Bundle bundle = proposed.getBundle();
         final BundleCoordinate coordinate = toCoordinate(bundle);
@@ -3619,7 +3634,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private Funnel addFunnel(final ProcessGroup destination, final VersionedFunnel proposed, final String componentIdSeed) {
-        final Funnel funnel = flowController.createFunnel(generateUuid(componentIdSeed));
+        final Funnel funnel = flowController.createFunnel(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed));
         funnel.setVersionedComponentId(proposed.getIdentifier());
         destination.addFunnel(funnel);
         updateFunnel(funnel, proposed);
@@ -3634,7 +3649,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private Port addInputPort(final ProcessGroup destination, final VersionedPort proposed, final String componentIdSeed) {
-        final Port port = flowController.createLocalInputPort(generateUuid(componentIdSeed), proposed.getName());
+        final Port port = flowController.createLocalInputPort(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), proposed.getName());
         port.setVersionedComponentId(proposed.getIdentifier());
         destination.addInputPort(port);
         updatePort(port, proposed);
@@ -3643,7 +3658,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private Port addOutputPort(final ProcessGroup destination, final VersionedPort proposed, final String componentIdSeed) {
-        final Port port = flowController.createLocalOutputPort(generateUuid(componentIdSeed), proposed.getName());
+        final Port port = flowController.createLocalOutputPort(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), proposed.getName());
         port.setVersionedComponentId(proposed.getIdentifier());
         destination.addOutputPort(port);
         updatePort(port, proposed);
@@ -3652,7 +3667,7 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private Label addLabel(final ProcessGroup destination, final VersionedLabel proposed, final String componentIdSeed) {
-        final Label label = flowController.createLabel(generateUuid(componentIdSeed), proposed.getLabel());
+        final Label label = flowController.createLabel(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), proposed.getLabel());
         label.setVersionedComponentId(proposed.getIdentifier());
         destination.addLabel(label);
         updateLabel(label, proposed);
@@ -3669,7 +3684,7 @@ public final class StandardProcessGroup implements ProcessGroup {
 
     private ProcessorNode addProcessor(final ProcessGroup destination, final VersionedProcessor proposed, final String componentIdSeed) throws ProcessorInstantiationException {
         final BundleCoordinate coordinate = toCoordinate(proposed.getBundle());
-        final ProcessorNode procNode = flowController.createProcessor(proposed.getType(), generateUuid(componentIdSeed), coordinate, true);
+        final ProcessorNode procNode = flowController.createProcessor(proposed.getType(), generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), coordinate, true);
         procNode.setVersionedComponentId(proposed.getIdentifier());
 
         destination.addProcessor(procNode);
@@ -3717,25 +3732,25 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private RemoteProcessGroup addRemoteProcessGroup(final ProcessGroup destination, final VersionedRemoteProcessGroup proposed, final String componentIdSeed) {
-        final RemoteProcessGroup rpg = flowController.createRemoteProcessGroup(generateUuid(componentIdSeed), proposed.getTargetUris());
+        final RemoteProcessGroup rpg = flowController.createRemoteProcessGroup(generateUuid(proposed.getIdentifier(), destination.getIdentifier(), componentIdSeed), proposed.getTargetUris());
         rpg.setVersionedComponentId(proposed.getIdentifier());
 
         destination.addRemoteProcessGroup(rpg);
-        updateRemoteProcessGroup(rpg, proposed);
+        updateRemoteProcessGroup(rpg, proposed, componentIdSeed);
 
         return rpg;
     }
 
-    private void updateRemoteProcessGroup(final RemoteProcessGroup rpg, final VersionedRemoteProcessGroup proposed) {
+    private void updateRemoteProcessGroup(final RemoteProcessGroup rpg, final VersionedRemoteProcessGroup proposed, final String componentIdSeed) {
         rpg.setComments(proposed.getComments());
         rpg.setCommunicationsTimeout(proposed.getCommunicationsTimeout());
         rpg.setInputPorts(proposed.getInputPorts() == null ? Collections.emptySet() : proposed.getInputPorts().stream()
-            .map(port -> createPortDescriptor(port))
+            .map(port -> createPortDescriptor(port, componentIdSeed, rpg.getIdentifier()))
             .collect(Collectors.toSet()));
         rpg.setName(proposed.getName());
         rpg.setNetworkInterface(proposed.getLocalNetworkInterface());
         rpg.setOutputPorts(proposed.getOutputPorts() == null ? Collections.emptySet() : proposed.getOutputPorts().stream()
-            .map(port -> createPortDescriptor(port))
+            .map(port -> createPortDescriptor(port, componentIdSeed, rpg.getIdentifier()))
             .collect(Collectors.toSet()));
         rpg.setPosition(new Position(proposed.getPosition().getX(), proposed.getPosition().getY()));
         rpg.setProxyHost(proposed.getProxyHost());
@@ -3745,16 +3760,22 @@ public final class StandardProcessGroup implements ProcessGroup {
         rpg.setYieldDuration(proposed.getYieldDuration());
     }
 
-    private RemoteProcessGroupPortDescriptor createPortDescriptor(final VersionedRemoteGroupPort proposed) {
+    private RemoteProcessGroupPortDescriptor createPortDescriptor(final VersionedRemoteGroupPort proposed, final String componentIdSeed, final String rpgId) {
         final StandardRemoteProcessGroupPortDescriptor descriptor = new StandardRemoteProcessGroupPortDescriptor();
         descriptor.setVersionedComponentId(proposed.getIdentifier());
-        descriptor.setBatchCount(proposed.getBatchSize().getCount());
-        descriptor.setBatchDuration(proposed.getBatchSize().getDuration());
-        descriptor.setBatchSize(proposed.getBatchSize().getSize());
+
+        final BatchSize batchSize = proposed.getBatchSize();
+        if (batchSize != null) {
+            descriptor.setBatchCount(batchSize.getCount());
+            descriptor.setBatchDuration(batchSize.getDuration());
+            descriptor.setBatchSize(batchSize.getSize());
+        }
+
         descriptor.setComments(proposed.getComments());
         descriptor.setConcurrentlySchedulableTaskCount(proposed.getConcurrentlySchedulableTaskCount());
-        descriptor.setGroupId(proposed.getGroupId());
-        descriptor.setId(UUID.randomUUID().toString()); // TODO: Need to address this issue of port id's
+        descriptor.setGroupId(proposed.getRemoteGroupId());
+        descriptor.setTargetId(proposed.getTargetId());
+        descriptor.setId(generateUuid(proposed.getIdentifier(), rpgId, componentIdSeed));
         descriptor.setName(proposed.getName());
         descriptor.setUseCompression(proposed.isUseCompression());
         return descriptor;
@@ -3785,29 +3806,8 @@ public final class StandardProcessGroup implements ProcessGroup {
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
         final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), false);
 
-        final ComparableDataFlow currentFlow = new ComparableDataFlow() {
-            @Override
-            public VersionedProcessGroup getContents() {
-                return versionedGroup;
-            }
-
-            @Override
-            public String getName() {
-                return "Local Flow";
-            }
-        };
-
-        final ComparableDataFlow snapshotFlow = new ComparableDataFlow() {
-            @Override
-            public VersionedProcessGroup getContents() {
-                return vci.getFlowSnapshot();
-            }
-
-            @Override
-            public String getName() {
-                return "Versioned Flow";
-            }
-        };
+        final ComparableDataFlow currentFlow = new StandardComparableDataFlow("Local Flow", versionedGroup);
+        final ComparableDataFlow snapshotFlow = new StandardComparableDataFlow("Versioned Flow", vci.getFlowSnapshot());
 
         final FlowComparator flowComparator = new StandardFlowComparator(snapshotFlow, currentFlow, new EvolvingDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
index 1d3eec6..1147b9e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
@@ -231,6 +231,7 @@ public class RestBasedFlowRegistry implements FlowRegistry {
             group.setProcessors(contents.getProcessors());
             group.setRemoteProcessGroups(contents.getRemoteProcessGroups());
             group.setVariables(contents.getVariables());
+            coordinates.setLatest(snapshot.isLatest());
         }
 
         for (final VersionedProcessGroup child : group.getProcessGroups()) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
index c3c1037..193bde8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryDtoMapper.java
@@ -309,10 +309,11 @@ public class NiFiRegistryDtoMapper {
         port.setGroupIdentifier(getGroupId(dto.getGroupId()));
         port.setComments(dto.getComments());
         port.setConcurrentlySchedulableTaskCount(dto.getConcurrentlySchedulableTaskCount());
-        port.setGroupId(dto.getGroupId());
+        port.setRemoteGroupId(dto.getGroupId());
         port.setName(dto.getName());
         port.setUseCompression(dto.getUseCompression());
-        port.setBatchSettings(mapBatchSettings(dto.getBatchSettings()));
+        port.setBatchSize(mapBatchSettings(dto.getBatchSettings()));
+        port.setTargetId(dto.getTargetId());
         port.setComponentType(componentType);
         return port;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
index a10a1b8..7bab76d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
@@ -310,7 +310,26 @@ public class NiFiRegistryFlowMapper {
         }
 
         component.setComments(connectable.getComments());
-        component.setGroupId(connectable.getProcessGroupIdentifier());
+        if (connectable instanceof RemoteGroupPort) {
+            final RemoteGroupPort port = (RemoteGroupPort) connectable;
+            final RemoteProcessGroup rpg = port.getRemoteProcessGroup();
+            final Optional<String> rpgVersionedId = rpg.getVersionedComponentId();
+            final String groupId;
+            if (rpgVersionedId.isPresent()) {
+                groupId = rpgVersionedId.get();
+            } else {
+                final String resolved = versionedComponentIds.get(rpg.getIdentifier());
+                if (resolved == null) {
+                    throw new IllegalArgumentException("Unable to find the Versioned Component ID for Remote Process Group that " + connectable + " belongs to");
+                }
+
+                groupId = resolved;
+            }
+
+            component.setGroupId(groupId);
+        } else {
+            component.setGroupId(connectable.getProcessGroupIdentifier());
+        }
         component.setName(connectable.getName());
         component.setType(ConnectableComponentType.valueOf(connectable.getConnectableType().name()));
         return component;
@@ -478,10 +497,11 @@ public class NiFiRegistryFlowMapper {
         port.setGroupIdentifier(getGroupId(remotePort.getRemoteProcessGroup().getIdentifier()));
         port.setComments(remotePort.getComments());
         port.setConcurrentlySchedulableTaskCount(remotePort.getMaxConcurrentTasks());
-        port.setGroupId(remotePort.getProcessGroupIdentifier());
+        port.setRemoteGroupId(getGroupId(remotePort.getRemoteProcessGroup().getIdentifier()));
         port.setName(remotePort.getName());
         port.setUseCompression(remotePort.isUseCompression());
-        port.setBatchSettings(mapBatchSettings(remotePort));
+        port.setBatchSize(mapBatchSettings(remotePort));
+        port.setTargetId(remotePort.getTargetIdentifier());
         port.setComponentType(componentType);
         return port;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java
new file mode 100644
index 0000000..fe92f91
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/StandardComparableDataFlow.java
@@ -0,0 +1,42 @@
+/*
+ * 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.registry.flow.mapping;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
+
+public class StandardComparableDataFlow implements ComparableDataFlow {
+    private final String name;
+    private final VersionedProcessGroup contents;
+
+    public StandardComparableDataFlow(final String name, final VersionedProcessGroup contents) {
+        this.name = name;
+        this.contents = contents;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public VersionedProcessGroup getContents() {
+        return contents;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
index 726daa0..039ac66 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/remote/StandardRemoteProcessGroup.java
@@ -780,6 +780,7 @@ public class StandardRemoteProcessGroup implements RemoteProcessGroup {
             if (!StringUtils.isBlank(descriptor.getBatchDuration())) {
                 port.setBatchDuration(descriptor.getBatchDuration());
             }
+            port.setVersionedComponentId(descriptor.getVersionedComponentId());
 
             inputPorts.put(descriptor.getId(), port);
             return port;

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
index 31f1fbe..30294a4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
@@ -300,6 +300,7 @@ public class FingerprintFactoryTest {
         // Assert fingerprints with expected one.
         final String expected = "portId" +
                 "NO_VALUE" +
+                "NO_VALUE" +
                 "3" +
                 "true" +
                 "1234" +

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index c66aebb..e0594fa 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -3756,29 +3756,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, false);
         final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents();
 
-        final ComparableDataFlow localFlow = new ComparableDataFlow() {
-            @Override
-            public VersionedProcessGroup getContents() {
-                return localGroup;
-            }
-
-            @Override
-            public String getName() {
-                return "Local Flow";
-            }
-        };
-
-        final ComparableDataFlow registryFlow = new ComparableDataFlow() {
-            @Override
-            public VersionedProcessGroup getContents() {
-                return registryGroup;
-            }
-
-            @Override
-            public String getName() {
-                return "Versioned Flow";
-            }
-        };
+        final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localGroup);
+        final ComparableDataFlow registryFlow = new StandardComparableDataFlow("Versioned Flow", registryGroup);
 
         final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, new ConciseEvolvingDifferenceDescriptor());
         final FlowComparison flowComparison = flowComparator.compare();
@@ -4037,7 +4016,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         }
 
         if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_PROCESS_GROUP.name())) {
-            return authorizableLookup.getRemoteProcessGroup(versionedComponent.getInstanceGroupId());
+            return authorizableLookup.getRemoteProcessGroup(componentId);
         }
 
         return null;

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index 6bf4cca..0ae5864 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -604,24 +604,6 @@ public class FlowResource extends ApplicationResource {
                             componentIds.add(outputPort.getIdentifier());
                         });
 
-                // ensure authorized for each remote input port we will attempt to schedule
-                group.findAllRemoteProcessGroups().stream()
-                    .flatMap(rpg -> rpg.getInputPorts().stream())
-                    .filter(ScheduledState.RUNNING.equals(state) ? ProcessGroup.SCHEDULABLE_PORTS : ProcessGroup.UNSCHEDULABLE_PORTS)
-                    .filter(port -> port.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
-                    .forEach(port -> {
-                        componentIds.add(port.getIdentifier());
-                    });
-
-                // ensure authorized for each remote output port we will attempt to schedule
-                group.findAllRemoteProcessGroups().stream()
-                    .flatMap(rpg -> rpg.getOutputPorts().stream())
-                    .filter(ScheduledState.RUNNING.equals(state) ? ProcessGroup.SCHEDULABLE_PORTS : ProcessGroup.UNSCHEDULABLE_PORTS)
-                    .filter(port -> port.isAuthorized(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()))
-                    .forEach(port -> {
-                        componentIds.add(port.getIdentifier());
-                    });
-
                 return componentIds;
             });
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 7262a82..3fa4462 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -1641,7 +1641,7 @@ public class ProcessGroupResource extends ApplicationResource {
         // Step 6: Replicate the request or call serviceFacade.updateProcessGroup
 
         final VersionControlInformationDTO versionControlInfo = requestProcessGroupEntity.getComponent().getVersionControlInformation();
-        if (versionControlInfo != null) {
+        if (versionControlInfo != null && requestProcessGroupEntity.getVersionedFlowSnapshot() == null) {
             // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
             // Step 2: Retrieve flow from Flow Registry
             final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(versionControlInfo, true);

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index f2a207e..6e61182 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -709,7 +709,7 @@ public class VersionsResource extends ApplicationResource {
                 final VersionControlInformationDTO versionControlInfoDto = new VersionControlInformationDTO();
                 versionControlInfoDto.setBucketId(snapshotMetadata.getBucketIdentifier());
                 versionControlInfoDto.setBucketName(bucket.getName());
-                versionControlInfoDto.setCurrent(true);
+                versionControlInfoDto.setCurrent(snapshotMetadata.getVersion() == flow.getVersionCount());
                 versionControlInfoDto.setFlowId(snapshotMetadata.getFlowIdentifier());
                 versionControlInfoDto.setFlowName(flow.getName());
                 versionControlInfoDto.setFlowDescription(flow.getDescription());
@@ -1152,7 +1152,7 @@ public class VersionsResource extends ApplicationResource {
         final String idGenerationSeed = getIdGenerationSeed().orElse(null);
 
         // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
-        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation(), false);
+        final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation(), true);
 
         // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
         // the flow snapshot to contain compatible bundles.
@@ -1217,7 +1217,7 @@ public class VersionsResource extends ApplicationResource {
                 final Consumer<AsynchronousWebRequest<VersionControlInformationEntity>> updateTask = vcur -> {
                     try {
                         final VersionControlInformationEntity updatedVersionControlEntity = updateFlowVersion(groupId, componentLifecycle, exampleUri,
-                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false, false);
+                            affectedComponents, user, replicateRequest, requestEntity, flowSnapshot, request, idGenerationSeed, false, true);
 
                         vcur.markComplete(updatedVersionControlEntity);
                     } catch (final LifecycleManagementException e) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/e1606701/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 7d40473..fb60604 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -3428,6 +3428,7 @@ public final class DtoFactory {
             batchCopy.setCount(batchOrg.getCount());
             batchCopy.setSize(batchOrg.getSize());
             batchCopy.setDuration(batchOrg.getDuration());
+            copy.setBatchSettings(batchCopy);
         }
         return copy;
     }


[18/50] nifi git commit: NIFI-4436: Bug fixes - Checkpoint before allowing multiple Process Groups with same Versioned Component ID and same parent - Ensure that if flow update is cancelled while processors are being stopped/services disabled that we sto

Posted by bb...@apache.org.
NIFI-4436: Bug fixes - Checkpoint before allowing multiple Process Groups with same Versioned Component ID and same parent - Ensure that if flow update is cancelled while processors are being stopped/services disabled that we stop waiting for that to occur. Also ensure that if we fail to update flow that we re-enable/restart the processors and services - Updated verbiage to use a ConciseEvolvingDifferentDescriptor when getting local modifications for a versioned flow - Do not allow outer process group to be saved to flow registry or have local modifications reverted if it has a descendant process group that is under version control and is dirty. Fixed bug where ComponentDifferenceDTO was populated with wrong component id and group id

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/adacb204
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/adacb204
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/adacb204

Branch: refs/heads/master
Commit: adacb204a8b6518a79600253463a36c9f8afaa37
Parents: 3d8b1e4
Author: Mark Payne <ma...@hotmail.com>
Authored: Fri Nov 17 11:02:33 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:53 2018 -0500

----------------------------------------------------------------------
 .../api/entity/VersionedFlowSnapshotEntity.java |  11 +
 .../org/apache/nifi/groups/ProcessGroup.java    |  27 +-
 .../apache/nifi/registry/flow/FlowRegistry.java |   9 +-
 .../apache/nifi/controller/FlowController.java  |  54 +++-
 .../nifi/groups/StandardProcessGroup.java       | 143 ++++++++--
 .../registry/flow/RestBasedFlowRegistry.java    |  17 +-
 .../service/mock/MockProcessGroup.java          |  14 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  34 ++-
 .../nifi/web/StandardNiFiServiceFacade.java     |  93 ++-----
 .../nifi/web/api/ProcessGroupResource.java      |  11 +-
 .../apache/nifi/web/api/VersionsResource.java   | 273 +++++++++----------
 .../api/concurrent/AsynchronousWebRequest.java  |   8 +
 .../StandardAsynchronousWebRequest.java         |   7 +
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  15 +-
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |   4 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |  10 +-
 .../nifi/web/util/CancellableTimedPause.java    |   2 +-
 .../org/apache/nifi/web/util/SnippetUtils.java  |  16 +-
 18 files changed, 490 insertions(+), 258 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
index 170640d..2faf791 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VersionedFlowSnapshotEntity.java
@@ -28,6 +28,7 @@ public class VersionedFlowSnapshotEntity extends Entity {
     private VersionedFlowSnapshot versionedFlowSnapshot;
     private RevisionDTO processGroupRevision;
     private String registryId;
+    private Boolean updateDescendantVersionedFlows;
 
     @ApiModelProperty("The versioned flow snapshot")
     public VersionedFlowSnapshot getVersionedFlowSnapshot() {
@@ -55,4 +56,14 @@ public class VersionedFlowSnapshotEntity extends Entity {
     public void setRegistryId(String registryId) {
         this.registryId = registryId;
     }
+
+    @ApiModelProperty("If the Process Group to be updated has a child or descendant Process Group that is also under "
+        + "Version Control, this specifies whether or not the contents of that child/descendant Process Group should be updated.")
+    public Boolean getUpdateDescendantVersionedFlows() {
+        return updateDescendantVersionedFlows;
+    }
+
+    public void setUpdateDescendantVersionedFlows(Boolean update) {
+        this.updateDescendantVersionedFlows = update;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index 16b4b5e..d81b7d3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -784,8 +784,10 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
      *            and the Process Group has been modified since it was last synchronized with the Flow Registry, then this method will
      *            throw an IllegalStateException
      * @param updateSettings whether or not to update the process group's name and positions
+     * @param updateDescendantVersionedFlows if a child/descendant Process Group is under Version Control, specifies whether or not to
+     *            update the contents of that Process Group
      */
-    void updateFlow(VersionedFlowSnapshot proposedSnapshot, String componentIdSeed, boolean verifyNotDirty, boolean updateSettings);
+    void updateFlow(VersionedFlowSnapshot proposedSnapshot, String componentIdSeed, boolean verifyNotDirty, boolean updateSettings, boolean updateDescendantVersionedFlows);
 
     /**
      * Verifies a template with the specified name can be created.
@@ -848,7 +850,7 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
     void verifyCanUpdateVariables(Map<String, String> updatedVariables);
 
     /**
-     * Ensure that the contents of the Process Group can be update to match the given new flow
+     * Ensures that the contents of the Process Group can be update to match the given new flow
      *
      * @param updatedFlow the updated version of the flow
      * @param verifyConnectionRemoval whether or not to verify that connections that are not present in the updated flow can be removed
@@ -860,6 +862,27 @@ public interface ProcessGroup extends ComponentAuthorizable, Positionable, Versi
     void verifyCanUpdate(VersionedFlowSnapshot updatedFlow, boolean verifyConnectionRemoval, boolean verifyNotDirty);
 
     /**
+     * Ensures that the Process Group can have any local changes reverted
+     *
+     * @throws IllegalStateException if the Process Group is not in a state that will allow local changes to be reverted
+     */
+    void verifyCanRevertLocalModifications();
+
+    /**
+     * Ensures that the Process Group can have its local modifications shown
+     *
+     * @throws IllegalStateException if the Process Group is not in a state that will allow local modifications to be shown
+     */
+    void verifyCanShowLocalModifications();
+
+    /**
+     * Ensure that the contents of the Process Group can be saved to a Flow Registry in its current state
+     *
+     * @throws IllegalStateException if the Process Group cannot currently be saved to a Flow Registry
+     */
+    void verifyCanSaveToFlowRegistry(String registryId, String bucketId, String flowId);
+
+    /**
      * Adds the given template to this Process Group
      *
      * @param template the template to add

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
index 76f96f2..ae43bb5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistry.java
@@ -159,6 +159,9 @@ public interface FlowRegistry {
      * @param bucketId the ID of the bucket
      * @param flowId the ID of the flow
      * @param version the version to retrieve
+     * @param fetchRemoteFlows if the remote flow has a child Process Group that also tracks to a remote flow, this specifies whether or not
+     *            the child's contents should be fetched.
+     * @param user the user on whose behalf the flow contents are being retrieved
      * @return the contents of the Flow from the Flow Registry
      *
      * @throws IOException if unable to communicate with the Flow Registry
@@ -167,7 +170,7 @@ public interface FlowRegistry {
      * @throws NullPointerException if any of the arguments is not specified
      * @throws IllegalArgumentException if the given version is less than 1
      */
-    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version, NiFiUser user) throws IOException, NiFiRegistryException;
+    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version, boolean fetchRemoteFlows, NiFiUser user) throws IOException, NiFiRegistryException;
 
     /**
      * Retrieves the contents of the Flow with the given Bucket ID, Flow ID, and version, from the Flow Registry
@@ -175,6 +178,8 @@ public interface FlowRegistry {
      * @param bucketId the ID of the bucket
      * @param flowId the ID of the flow
      * @param version the version to retrieve
+     * @param fetchRemoteFlows if the remote flow has a child Process Group that also tracks to a remote flow, this specifies whether or not
+     *            the child's contents should be fetched.
      * @return the contents of the Flow from the Flow Registry
      *
      * @throws IOException if unable to communicate with the Flow Registry
@@ -183,7 +188,7 @@ public interface FlowRegistry {
      * @throws NullPointerException if any of the arguments is not specified
      * @throws IllegalArgumentException if the given version is less than 1
      */
-    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version) throws IOException, NiFiRegistryException;
+    VersionedFlowSnapshot getFlowContents(String bucketId, String flowId, int version, boolean fetchRemoteFlows) throws IOException, NiFiRegistryException;
 
     /**
      * Retrieves a VersionedFlow by bucket id and flow id

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 5ed5b6e..3909387 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -165,8 +165,11 @@ import org.apache.nifi.provenance.StandardProvenanceEventRecord;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
+import org.apache.nifi.registry.flow.StandardVersionControlInformation;
+import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.registry.variable.StandardComponentVariableRegistry;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
@@ -1775,6 +1778,10 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
      * processor
      */
     public void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto) throws ProcessorInstantiationException {
+        instantiateSnippet(group, dto, true);
+    }
+
+    private void instantiateSnippet(final ProcessGroup group, final FlowSnippetDTO dto, final boolean topLevel) throws ProcessorInstantiationException {
         writeLock.lock();
         try {
             validateSnippetContents(requireNonNull(group), dto);
@@ -1789,6 +1796,9 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 serviceNode.setAnnotationData(controllerServiceDTO.getAnnotationData());
                 serviceNode.setComments(controllerServiceDTO.getComments());
                 serviceNode.setName(controllerServiceDTO.getName());
+                if (!topLevel) {
+                    serviceNode.setVersionedComponentId(controllerServiceDTO.getVersionedComponentId());
+                }
 
                 group.addControllerService(serviceNode);
             }
@@ -1812,6 +1822,10 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 }
 
                 label.setStyle(labelDTO.getStyle());
+                if (!topLevel) {
+                    label.setVersionedComponentId(labelDTO.getVersionedComponentId());
+                }
+
                 group.addLabel(label);
             }
 
@@ -1819,6 +1833,10 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
             for (final FunnelDTO funnelDTO : dto.getFunnels()) {
                 final Funnel funnel = createFunnel(funnelDTO.getId());
                 funnel.setPosition(toPosition(funnelDTO.getPosition()));
+                if (!topLevel) {
+                    funnel.setVersionedComponentId(funnelDTO.getVersionedComponentId());
+                }
+
                 group.addFunnel(funnel);
             }
 
@@ -1840,6 +1858,9 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                     inputPort = createLocalInputPort(portDTO.getId(), portDTO.getName());
                 }
 
+                if (!topLevel) {
+                    inputPort.setVersionedComponentId(portDTO.getVersionedComponentId());
+                }
                 inputPort.setPosition(toPosition(portDTO.getPosition()));
                 inputPort.setProcessGroup(group);
                 inputPort.setComments(portDTO.getComments());
@@ -1861,6 +1882,9 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                     outputPort = createLocalOutputPort(portDTO.getId(), portDTO.getName());
                 }
 
+                if (!topLevel) {
+                    outputPort.setVersionedComponentId(portDTO.getVersionedComponentId());
+                }
                 outputPort.setPosition(toPosition(portDTO.getPosition()));
                 outputPort.setProcessGroup(group);
                 outputPort.setComments(portDTO.getComments());
@@ -1876,6 +1900,9 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
 
                 procNode.setPosition(toPosition(processorDTO.getPosition()));
                 procNode.setProcessGroup(group);
+                if (!topLevel) {
+                    procNode.setVersionedComponentId(processorDTO.getVersionedComponentId());
+                }
 
                 final ProcessorConfigDTO config = processorDTO.getConfig();
                 procNode.setComments(config.getComments());
@@ -1936,6 +1963,10 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 remoteGroup.setPosition(toPosition(remoteGroupDTO.getPosition()));
                 remoteGroup.setCommunicationsTimeout(remoteGroupDTO.getCommunicationsTimeout());
                 remoteGroup.setYieldDuration(remoteGroupDTO.getYieldDuration());
+                if (!topLevel) {
+                    remoteGroup.setVersionedComponentId(remoteGroupDTO.getVersionedComponentId());
+                }
+
                 if (remoteGroupDTO.getTransportProtocol() == null) {
                     remoteGroup.setTransportProtocol(SiteToSiteTransportProtocol.RAW);
                 } else {
@@ -1979,6 +2010,12 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                     childGroup.setVariables(groupDTO.getVariables());
                 }
 
+                // If this Process Group is 'top level' then we do not set versioned component ID's.
+                // We do this only if this component is the child of a Versioned Component.
+                if (!topLevel) {
+                    childGroup.setVersionedComponentId(groupDTO.getVersionedComponentId());
+                }
+
                 group.addProcessGroup(childGroup);
 
                 final FlowSnippetDTO contents = groupDTO.getContents();
@@ -1995,7 +2032,18 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 childTemplateDTO.setFunnels(contents.getFunnels());
                 childTemplateDTO.setRemoteProcessGroups(contents.getRemoteProcessGroups());
                 childTemplateDTO.setControllerServices(contents.getControllerServices());
-                instantiateSnippet(childGroup, childTemplateDTO);
+                instantiateSnippet(childGroup, childTemplateDTO, false);
+
+                if (groupDTO.getVersionControlInformation() != null) {
+                    final NiFiRegistryFlowMapper flowMapper = new NiFiRegistryFlowMapper();
+                    final VersionedProcessGroup versionedGroup = flowMapper.mapProcessGroup(childGroup, getFlowRegistryClient(), false);
+
+                    final VersionControlInformation vci = StandardVersionControlInformation.Builder
+                        .fromDto(groupDTO.getVersionControlInformation())
+                        .flowSnapshot(versionedGroup)
+                        .build();
+                    childGroup.setVersionControlInformation(vci, Collections.emptyMap());
+                }
             }
 
             //
@@ -2039,6 +2087,9 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
                 }
 
                 final Connection connection = createConnection(connectionDTO.getId(), connectionDTO.getName(), source, destination, relationships);
+                if (!topLevel) {
+                    connection.setVersionedComponentId(connectionDTO.getVersionedComponentId());
+                }
 
                 if (connectionDTO.getBends() != null) {
                     final List<Position> bendPoints = new ArrayList<>();
@@ -2088,6 +2139,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
             for (final RemoteProcessGroupPortDTO port : ports) {
                 final StandardRemoteProcessGroupPortDescriptor descriptor = new StandardRemoteProcessGroupPortDescriptor();
                 descriptor.setId(port.getId());
+                descriptor.setVersionedComponentId(port.getVersionedComponentId());
                 descriptor.setTargetId(port.getTargetId());
                 descriptor.setName(port.getName());
                 descriptor.setComments(port.getComments());

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index d1aa4e2..51839d0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -2821,7 +2821,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             versionControlInformation.getBucketIdentifier(),
             versionControlInformation.getFlowIdentifier(),
             versionControlInformation.getVersion(),
-            versionControlInformation.getFlowSnapshot(),
+            stripContentsFromRemoteDescendantGroups(versionControlInformation.getFlowSnapshot(), true),
             versionControlInformation.isModified(),
             versionControlInformation.isCurrent()) {
 
@@ -2849,6 +2849,51 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
     }
 
+    private VersionedProcessGroup stripContentsFromRemoteDescendantGroups(final VersionedProcessGroup processGroup, final boolean topLevel) {
+        if (processGroup == null) {
+            return null;
+        }
+
+        final VersionedProcessGroup copy = new VersionedProcessGroup();
+        copy.setComments(processGroup.getComments());
+        copy.setComponentType(processGroup.getComponentType());
+        copy.setGroupIdentifier(processGroup.getGroupIdentifier());
+        copy.setIdentifier(processGroup.getIdentifier());
+        copy.setName(processGroup.getName());
+        copy.setPosition(processGroup.getPosition());
+        copy.setVersionedFlowCoordinates(topLevel ? null : processGroup.getVersionedFlowCoordinates());
+        copy.setConnections(processGroup.getConnections());
+        copy.setControllerServices(processGroup.getControllerServices());
+        copy.setFunnels(processGroup.getFunnels());
+        copy.setInputPorts(processGroup.getInputPorts());
+        copy.setOutputPorts(processGroup.getOutputPorts());
+        copy.setProcessors(processGroup.getProcessors());
+        copy.setRemoteProcessGroups(processGroup.getRemoteProcessGroups());
+        copy.setVariables(processGroup.getVariables());
+
+        final Set<VersionedProcessGroup> copyChildren = new HashSet<>();
+
+        for (final VersionedProcessGroup childGroup : processGroup.getProcessGroups()) {
+            if (childGroup.getVersionedFlowCoordinates() == null) {
+                copyChildren.add(stripContentsFromRemoteDescendantGroups(childGroup, false));
+            } else {
+                final VersionedProcessGroup childCopy = new VersionedProcessGroup();
+                childCopy.setComments(childGroup.getComments());
+                childCopy.setComponentType(childGroup.getComponentType());
+                childCopy.setGroupIdentifier(childGroup.getGroupIdentifier());
+                childCopy.setIdentifier(childGroup.getIdentifier());
+                childCopy.setName(childGroup.getName());
+                childCopy.setPosition(childGroup.getPosition());
+                childCopy.setVersionedFlowCoordinates(childGroup.getVersionedFlowCoordinates());
+
+                copyChildren.add(childCopy);
+            }
+        }
+
+        copy.setProcessGroups(copyChildren);
+        return copy;
+    }
+
     @Override
     public void disconnectVersionControl() {
         writeLock.lock();
@@ -2900,7 +2945,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             });
 
         processGroup.getProcessGroups().stream()
-            .filter(childGroup -> childGroup.getVersionControlInformation() != null)
+            .filter(childGroup -> childGroup.getVersionControlInformation() == null)
             .forEach(childGroup -> applyVersionedComponentIds(childGroup, lookup));
     }
 
@@ -2925,7 +2970,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             // We have not yet obtained the snapshot from the Flow Registry, so we need to request the snapshot of our local version of the flow from the Flow Registry.
             // This allows us to know whether or not the flow has been modified since it was last synced with the Flow Registry.
             try {
-                final VersionedFlowSnapshot registrySnapshot = flowRegistry.getFlowContents(vci.getBucketIdentifier(), vci.getFlowIdentifier(), vci.getVersion());
+                final VersionedFlowSnapshot registrySnapshot = flowRegistry.getFlowContents(vci.getBucketIdentifier(), vci.getFlowIdentifier(), vci.getVersion(), false);
                 final VersionedProcessGroup registryFlow = registrySnapshot.getFlowContents();
                 vci.setFlowSnapshot(registryFlow);
             } catch (final IOException | NiFiRegistryException e) {
@@ -2958,7 +3003,8 @@ public final class StandardProcessGroup implements ProcessGroup {
 
 
     @Override
-    public void updateFlow(final VersionedFlowSnapshot proposedSnapshot, final String componentIdSeed, final boolean verifyNotDirty, final boolean updateSettings) {
+    public void updateFlow(final VersionedFlowSnapshot proposedSnapshot, final String componentIdSeed, final boolean verifyNotDirty, final boolean updateSettings,
+            final boolean updateDescendantVersionedFlows) {
         writeLock.lock();
         try {
             verifyCanUpdate(proposedSnapshot, true, verifyNotDirty);
@@ -2986,7 +3032,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
 
             final Set<String> knownVariables = getKnownVariableNames();
-            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings, knownVariables);
+            updateProcessGroup(this, proposedSnapshot.getFlowContents(), componentIdSeed, updatedVersionedComponentIds, false, updateSettings, updateDescendantVersionedFlows, knownVariables);
         } catch (final ProcessorInstantiationException pie) {
             throw new RuntimeException(pie);
         } finally {
@@ -3013,7 +3059,8 @@ public final class StandardProcessGroup implements ProcessGroup {
 
 
     private void updateProcessGroup(final ProcessGroup group, final VersionedProcessGroup proposed, final String componentIdSeed,
-        final Set<String> updatedVersionedComponentIds, final boolean updatePosition, final boolean updateName, final Set<String> variablesToSkip) throws ProcessorInstantiationException {
+        final Set<String> updatedVersionedComponentIds, final boolean updatePosition, final boolean updateName, final boolean updateDescendantVersionedGroups,
+        final Set<String> variablesToSkip) throws ProcessorInstantiationException {
 
         group.setComments(proposed.getComments());
 
@@ -3033,14 +3080,8 @@ public final class StandardProcessGroup implements ProcessGroup {
             .map(VariableDescriptor::getName)
             .collect(Collectors.toSet());
 
-        final Set<String> variablesRemoved = new HashSet<>(existingVariableNames);
-
-        if (proposed.getVariables() != null) {
-            variablesRemoved.removeAll(proposed.getVariables().keySet());
-        }
 
         final Map<String, String> updatedVariableMap = new HashMap<>();
-        variablesRemoved.forEach(var -> updatedVariableMap.put(var, null));
 
         // If any new variables exist in the proposed flow, add those to the variable registry.
         for (final Map.Entry<String, String> entry : proposed.getVariables().entrySet()) {
@@ -3069,6 +3110,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 .flowId(flowId)
                 .flowName(flowId) // flow id not yet known
                 .version(version)
+                .flowSnapshot(proposed)
                 .modified(false)
                 .current(true)
                 .build();
@@ -3084,11 +3126,13 @@ public final class StandardProcessGroup implements ProcessGroup {
         for (final VersionedProcessGroup proposedChildGroup : proposed.getProcessGroups()) {
             final ProcessGroup childGroup = childGroupsByVersionedId.get(proposedChildGroup.getIdentifier());
 
+            final VersionedFlowCoordinates childCoordinates = proposedChildGroup.getVersionedFlowCoordinates();
+
             if (childGroup == null) {
                 final ProcessGroup added = addProcessGroup(group, proposedChildGroup, componentIdSeed, variablesToSkip);
                 LOG.info("Added {} to {}", added, this);
-            } else {
-                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName, variablesToSkip);
+            } else if (childCoordinates == null || updateDescendantVersionedGroups) {
+                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName, updateDescendantVersionedGroups, variablesToSkip);
                 LOG.info("Updated {}", childGroup);
             }
 
@@ -3367,7 +3411,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         final ProcessGroup group = flowController.createProcessGroup(generateUuid(componentIdSeed));
         group.setVersionedComponentId(proposed.getIdentifier());
         group.setParent(destination);
-        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true, variablesToSkip);
+        updateProcessGroup(group, proposed, componentIdSeed, Collections.emptySet(), true, true, true, variablesToSkip);
         destination.addProcessGroup(group);
         return group;
     }
@@ -3739,7 +3783,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         }
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), true);
+        final VersionedProcessGroup versionedGroup = mapper.mapProcessGroup(this, flowController.getFlowRegistryClient(), false);
 
         final ComparableDataFlow currentFlow = new ComparableDataFlow() {
             @Override
@@ -3765,7 +3809,7 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
         };
 
-        final FlowComparator flowComparator = new StandardFlowComparator(currentFlow, snapshotFlow, new EvolvingDifferenceDescriptor());
+        final FlowComparator flowComparator = new StandardFlowComparator(snapshotFlow, currentFlow, new EvolvingDifferenceDescriptor());
         final FlowComparison comparison = flowComparator.compare();
         final Set<FlowDifference> differences = comparison.getDifferences();
         final Set<FlowDifference> functionalDifferences = differences.stream()
@@ -4002,4 +4046,69 @@ public final class StandardProcessGroup implements ProcessGroup {
             findAllProcessGroups(child, map);
         }
     }
+
+    @Override
+    public void verifyCanSaveToFlowRegistry(final String registryId, final String bucketId, final String flowId) {
+        verifyNoDescendantsWithLocalModifications("be saved to a Flow Registry");
+
+        final StandardVersionControlInformation vci = versionControlInfo.get();
+        if (vci != null) {
+            if (flowId != null && flowId.equals(vci.getFlowIdentifier())) {
+                // Flow ID is the same. We want to publish the Process Group as the next version of the Flow.
+                // In order to do this, we have to ensure that the Process Group is 'current'.
+                final boolean current = vci.isCurrent();
+                if (!current) {
+                    throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + getIdentifier()
+                        + " because the Process Group in the flow is not synchronized with the most recent version of the Flow in the Flow Registry. "
+                        + "In order to publish a new version of the Flow, the Process Group must first be in synch with the latest version in the Flow Registry.");
+                }
+
+                // Flow ID matches. We want to publish the Process Group as the next version of the Flow, so we must
+                // ensure that all other parameters match as well.
+                if (!bucketId.equals(vci.getBucketIdentifier())) {
+                    throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + getIdentifier()
+                        + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
+                }
+
+                if (!registryId.equals(vci.getRegistryIdentifier())) {
+                    throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + getIdentifier()
+                        + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
+                }
+            } else if (flowId != null) {
+                // Flow ID is specified but different. This is not allowed, because Flow ID's are automatically generated,
+                // and if the client is specifying an ID then it is either trying to assign the ID of the Flow or it is
+                // attempting to save a new version of a different flow. Saving a new version of a different Flow is
+                // not allowed because the Process Group must be in synch with the latest version of the flow before that
+                // can be done.
+                throw new IllegalStateException("Cannot update Version Control Information for Process Group with ID " + getIdentifier()
+                    + " because the Process Group is currently synchronized with a different Versioned Flow than the one specified in the request.");
+            }
+        }
+    }
+
+    @Override
+    public void verifyCanRevertLocalModifications() {
+        final StandardVersionControlInformation svci = versionControlInfo.get();
+        if (svci == null) {
+            throw new IllegalStateException("Cannot revert local modifications to Process Group because the Process Group is not under Version Control.");
+        }
+
+        verifyNoDescendantsWithLocalModifications("have its local modifications reverted");
+    }
+
+    @Override
+    public void verifyCanShowLocalModifications() {
+
+    }
+
+    private void verifyNoDescendantsWithLocalModifications(final String action) {
+        for (final ProcessGroup descendant : findAllProcessGroups()) {
+            final VersionControlInformation descendantVci = descendant.getVersionControlInformation();
+            if (descendantVci != null && descendantVci.isModified()) {
+                throw new IllegalStateException("Process Group cannot " + action + " because it contains a child or descendant Process Group that is under Version Control and "
+                    + "has local modifications. Each descendant Process Group that is under Version Control must first be reverted or have its changes pushed to the Flow Registry before "
+                    + "this action can be performed on the parent Process Group.");
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
index 8bf89c6..1d3eec6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/RestBasedFlowRegistry.java
@@ -178,21 +178,24 @@ public class RestBasedFlowRegistry implements FlowRegistry {
     }
 
     @Override
-    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version, final NiFiUser user) throws IOException, NiFiRegistryException {
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version, final boolean fetchRemoteFlows, final NiFiUser user)
+            throws IOException, NiFiRegistryException {
         final FlowSnapshotClient snapshotClient = getRegistryClient().getFlowSnapshotClient(getIdentity(user));
         final VersionedFlowSnapshot flowSnapshot = snapshotClient.get(bucketId, flowId, version);
 
-        final VersionedProcessGroup contents = flowSnapshot.getFlowContents();
-        for (final VersionedProcessGroup child : contents.getProcessGroups()) {
-            populateVersionedContentsRecursively(child, user);
+        if (fetchRemoteFlows) {
+            final VersionedProcessGroup contents = flowSnapshot.getFlowContents();
+            for (final VersionedProcessGroup child : contents.getProcessGroups()) {
+                populateVersionedContentsRecursively(child, user);
+            }
         }
 
         return flowSnapshot;
     }
 
     @Override
-    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version) throws IOException, NiFiRegistryException {
-        return getFlowContents(bucketId, flowId, version, null);
+    public VersionedFlowSnapshot getFlowContents(final String bucketId, final String flowId, final int version, final boolean fetchRemoteFlows) throws IOException, NiFiRegistryException {
+        return getFlowContents(bucketId, flowId, version, fetchRemoteFlows, null);
     }
 
     private void populateVersionedContentsRecursively(final VersionedProcessGroup group, final NiFiUser user) throws NiFiRegistryException, IOException {
@@ -214,7 +217,7 @@ public class RestBasedFlowRegistry implements FlowRegistry {
             }
 
             final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
-            final VersionedFlowSnapshot snapshot = flowRegistry.getFlowContents(bucketId, flowId, version, user);
+            final VersionedFlowSnapshot snapshot = flowRegistry.getFlowContents(bucketId, flowId, version, true, user);
             final VersionedProcessGroup contents = snapshot.getFlowContents();
 
             group.setComments(contents.getComments());

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
index db4ac59..ef69906 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
@@ -650,11 +650,15 @@ public class MockProcessGroup implements ProcessGroup {
     }
 
     @Override
+    public void verifyCanSaveToFlowRegistry(String registryId, String bucketId, String flowId) {
+    }
+
+    @Override
     public void synchronizeWithFlowRegistry(FlowRegistryClient flowRegistry) {
     }
 
     @Override
-    public void updateFlow(VersionedFlowSnapshot proposedFlow, String componentIdSeed, boolean verifyNotDirty, boolean updateSettings) {
+    public void updateFlow(VersionedFlowSnapshot proposedFlow, String componentIdSeed, boolean verifyNotDirty, boolean updateSettings, boolean updateDescendantVerisonedFlows) {
     }
 
     @Override
@@ -666,4 +670,12 @@ public class MockProcessGroup implements ProcessGroup {
     public void disconnectVersionControl() {
         this.versionControlInfo = null;
     }
+
+    @Override
+    public void verifyCanRevertLocalModifications() {
+    }
+
+    @Override
+    public void verifyCanShowLocalModifications() {
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 76cd2c4..02df16b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -1368,11 +1368,13 @@ public interface NiFiServiceFacade {
      * Retrieves the Versioned Flow Snapshot for the coordinates provided by the given Version Control Information DTO
      *
      * @param versionControlInfo the coordinates of the versioned flow
+     * @param fetchRemoteFlows if the contents of Versioned Flow that is fetched contains a child/descendant Process Group
+     *            that is also under Version Control, this indicates whether that remote flow should also be fetched
      * @return the VersionedFlowSnapshot that corresponds to the given coordinates
      *
      * @throws ResourceNotFoundException if the Versioned Flow Snapshot could not be found
      */
-    VersionedFlowSnapshot getVersionedFlowSnapshot(VersionControlInformationDTO versionControlInfo) throws IOException;
+    VersionedFlowSnapshot getVersionedFlowSnapshot(VersionControlInformationDTO versionControlInfo, boolean fetchRemoteFlows) throws IOException;
 
     /**
      * Returns the name of the Flow Registry that is registered with the given ID. If no Flow Registry exists with the given ID, will return
@@ -1407,6 +1409,28 @@ public interface NiFiServiceFacade {
     void verifyCanUpdate(String groupId, VersionedFlowSnapshot proposedFlow, boolean verifyConnectionRemoval, boolean verifyNotDirty);
 
     /**
+     * Verifies that the Process Group with the given identifier can be saved to the flow registry
+     *
+     * @param groupId the ID of the Process Group
+     * @param registryId the ID of the Flow Registry
+     * @param bucketId the ID of the bucket
+     * @param flowId the ID of the flow
+     *
+     * @throws IllegalStateException if the Process Group cannot be saved to the flow registry with the coordinates specified
+     */
+    void verifyCanSaveToFlowRegistry(String groupId, String registryId, String bucketId, String flowId);
+
+    /**
+     * Verifies that the Process Group with the given identifier can have its local modifications reverted to the given VersionedFlowSnapshot
+     *
+     * @param groupId the ID of the Process Group
+     * @param versionedFlowSnapshot the Versioned Flow Snapshot
+     *
+     * @throws IllegalStateException if the Process Group cannot have its local modifications reverted
+     */
+    void verifyCanRevertLocalModifications(String groupId, VersionedFlowSnapshot versionedFlowSnapshot);
+
+    /**
      * Updates the Process group with the given ID to match the new snapshot
      *
      * @param revision the revision of the Process Group
@@ -1414,10 +1438,12 @@ public interface NiFiServiceFacade {
      * @param versionControlInfo the Version Control information
      * @param snapshot the new snapshot
      * @param componentIdSeed the seed to use for generating new component ID's
+     * @param updateDescendantVersionedFlows if a child/descendant Process Group is under Version Control, specifies whether or not to
+     *            update the contents of that Process Group
      * @return the Process Group
      */
     ProcessGroupEntity updateProcessGroup(Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
-        boolean verifyNotModified);
+        boolean verifyNotModified, boolean updateDescendantVersionedFlows);
 
     /**
      * Updates the Process group with the given ID to match the new snapshot
@@ -1429,10 +1455,12 @@ public interface NiFiServiceFacade {
      * @param snapshot the new snapshot
      * @param componentIdSeed the seed to use for generating new component ID's
      * @param updateSettings whether or not the process group's name and position should be updated
+     * @param updateDescendantVersionedFlows if a child/descendant Process Group is under Version Control, specifies whether or not to
+     *            update the contents of that Process Group
      * @return the Process Group
      */
     ProcessGroupEntity updateProcessGroupContents(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
-        boolean verifyNotModified, boolean updateSettings);
+        boolean verifyNotModified, boolean updateSettings, boolean updateDescendantVersionedFlows);
 
     // ----------------------------------------
     // Component state methods

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 4d1bbbc..c66aebb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -97,13 +97,12 @@ import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedFlow;
-import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.ConciseEvolvingDifferenceDescriptor;
 import org.apache.nifi.registry.flow.diff.DifferenceType;
-import org.apache.nifi.registry.flow.diff.EvolvingDifferenceDescriptor;
 import org.apache.nifi.registry.flow.diff.FlowComparator;
 import org.apache.nifi.registry.flow.diff.FlowComparison;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
@@ -3751,10 +3750,10 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         }
 
         final VersionedFlowSnapshot versionedFlowSnapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketIdentifier(),
-            versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), NiFiUserUtils.getNiFiUser());
+            versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), false, NiFiUserUtils.getNiFiUser());
 
         final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper();
-        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, true);
+        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, flowRegistryClient, false);
         final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents();
 
         final ComparableDataFlow localFlow = new ComparableDataFlow() {
@@ -3781,7 +3780,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             }
         };
 
-        final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, new EvolvingDifferenceDescriptor());
+        final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, new ConciseEvolvingDifferenceDescriptor());
         final FlowComparison flowComparison = flowComparator.compare();
 
         final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison);
@@ -3853,6 +3852,23 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
+    public void verifyCanSaveToFlowRegistry(final String groupId, final String registryId, final String bucketId, final String flowId) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        group.verifyCanSaveToFlowRegistry(registryId, bucketId, flowId);
+    }
+
+    @Override
+    public void verifyCanRevertLocalModifications(final String groupId, final VersionedFlowSnapshot versionedFlowSnapshot) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        group.verifyCanRevertLocalModifications();
+
+        // verify that the process group can be updated to the given snapshot. We do not verify that connections can
+        // be removed, because the flow may still be running, and it only matters that the connections can be removed once the components
+        // have been stopped.
+        group.verifyCanUpdate(versionedFlowSnapshot, false, false);
+    }
+
+    @Override
     public Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(final String processGroupId, final VersionedFlowSnapshot updatedSnapshot, final NiFiUser user) throws IOException {
         final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
 
@@ -4028,7 +4044,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public VersionedFlowSnapshot getVersionedFlowSnapshot(final VersionControlInformationDTO versionControlInfo) throws IOException {
+    public VersionedFlowSnapshot getVersionedFlowSnapshot(final VersionControlInformationDTO versionControlInfo, final boolean fetchRemoteFlows) throws IOException {
         final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(versionControlInfo.getRegistryId());
         if (flowRegistry == null) {
             throw new ResourceNotFoundException("Could not find any Flow Registry registered with identifier " + versionControlInfo.getRegistryId());
@@ -4036,15 +4052,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
         final VersionedFlowSnapshot snapshot;
         try {
-            snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion(), NiFiUserUtils.getNiFiUser());
+            snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion(), fetchRemoteFlows, NiFiUserUtils.getNiFiUser());
         } catch (final NiFiRegistryException e) {
             throw new IllegalArgumentException("The Flow Registry with ID " + versionControlInfo.getRegistryId() + " reports that no Flow exists with Bucket "
                 + versionControlInfo.getBucketId() + ", Flow " + versionControlInfo.getFlowId() + ", Version " + versionControlInfo.getVersion());
         }
 
-        // If this Flow has a reference to a remote flow, we need to pull that remote flow as well
-        populateVersionedChildFlows(snapshot);
-
         return snapshot;
     }
 
@@ -4054,74 +4067,22 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return flowRegistry == null ? flowRegistryId : flowRegistry.getName();
     }
 
-    private void populateVersionedChildFlows(final VersionedFlowSnapshot snapshot) throws IOException {
-        final VersionedProcessGroup group = snapshot.getFlowContents();
-
-        for (final VersionedProcessGroup child : group.getProcessGroups()) {
-            populateVersionedFlows(child);
-        }
-    }
-
-    private void populateVersionedFlows(final VersionedProcessGroup group) throws IOException {
-        final VersionedFlowCoordinates remoteCoordinates = group.getVersionedFlowCoordinates();
-
-        if (remoteCoordinates != null) {
-            final String registryUrl = remoteCoordinates.getRegistryUrl();
-            final String registryId = flowRegistryClient.getFlowRegistryId(registryUrl);
-            if (registryId == null) {
-                throw new IllegalArgumentException("Process Group with ID " + group.getIdentifier() + " is under Version Control, referencing a Flow Registry at URL [" + registryUrl
-                    + "], but no Flow Registry is currently registered for that URL.");
-            }
-
-            final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryId);
-
-            final VersionedFlowSnapshot childSnapshot;
-            try {
-                childSnapshot = flowRegistry.getFlowContents(remoteCoordinates.getBucketId(), remoteCoordinates.getFlowId(), remoteCoordinates.getVersion(), NiFiUserUtils.getNiFiUser());
-            } catch (final NiFiRegistryException e) {
-                throw new IllegalArgumentException("The Flow Registry with ID " + registryId + " reports that no Flow exists with Bucket "
-                    + remoteCoordinates.getBucketId() + ", Flow " + remoteCoordinates.getFlowId() + ", Version " + remoteCoordinates.getVersion());
-            }
-
-            final VersionedProcessGroup fetchedGroup = childSnapshot.getFlowContents();
-            group.setComments(fetchedGroup.getComments());
-            group.setPosition(fetchedGroup.getPosition());
-            group.setName(fetchedGroup.getName());
-            group.setVariables(fetchedGroup.getVariables());
-
-            group.setConnections(new LinkedHashSet<>(fetchedGroup.getConnections()));
-            group.setControllerServices(new LinkedHashSet<>(fetchedGroup.getControllerServices()));
-            group.setFunnels(new LinkedHashSet<>(fetchedGroup.getFunnels()));
-            group.setInputPorts(new LinkedHashSet<>(fetchedGroup.getInputPorts()));
-            group.setLabels(new LinkedHashSet<>(fetchedGroup.getLabels()));
-            group.setOutputPorts(new LinkedHashSet<>(fetchedGroup.getOutputPorts()));
-            group.setProcessGroups(new LinkedHashSet<>(fetchedGroup.getProcessGroups()));
-            group.setProcessors(new LinkedHashSet<>(fetchedGroup.getProcessors()));
-            group.setRemoteProcessGroups(new LinkedHashSet<>(fetchedGroup.getRemoteProcessGroups()));
-        }
-
-        for (final VersionedProcessGroup child : group.getProcessGroups()) {
-            populateVersionedFlows(child);
-        }
-    }
-
-
     @Override
     public ProcessGroupEntity updateProcessGroup(final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
-        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified) {
+        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateDescendantVersionedFlows) {
 
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        return updateProcessGroupContents(user, revision, groupId, versionControlInfo, proposedFlowSnapshot, componentIdSeed, verifyNotModified, true);
+        return updateProcessGroupContents(user, revision, groupId, versionControlInfo, proposedFlowSnapshot, componentIdSeed, verifyNotModified, true, updateDescendantVersionedFlows);
     }
 
     @Override
     public ProcessGroupEntity updateProcessGroupContents(final NiFiUser user, final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
-        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings) {
+        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) {
 
         final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(groupId);
         final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(user, revision,
             processGroupNode,
-            () -> processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings),
+            () -> processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows),
             processGroup -> dtoFactory.createProcessGroupDto(processGroup));
 
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);

http://git-wip-us.apache.org/repos/asf/nifi/blob/adacb204/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index de56a4f..7262a82 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -1644,7 +1644,7 @@ public class ProcessGroupResource extends ApplicationResource {
         if (versionControlInfo != null) {
             // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
             // Step 2: Retrieve flow from Flow Registry
-            final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(versionControlInfo);
+            final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(versionControlInfo, true);
             final Bucket bucket = flowSnapshot.getBucket();
             final VersionedFlow flow = flowSnapshot.getFlow();
 
@@ -1653,6 +1653,8 @@ public class ProcessGroupResource extends ApplicationResource {
             versionControlInfo.setFlowDescription(flow.getDescription());
 
             versionControlInfo.setRegistryName(serviceFacade.getFlowRegistryName(versionControlInfo.getRegistryId()));
+            versionControlInfo.setModified(false);
+            versionControlInfo.setCurrent(flowSnapshot.isLatest());
 
             // Step 3: Resolve Bundle info
             BundleUtils.discoverCompatibleBundles(flowSnapshot.getFlowContents());
@@ -1709,8 +1711,13 @@ public class ProcessGroupResource extends ApplicationResource {
                         final RevisionDTO revisionDto = entity.getRevision();
                         final String newGroupId = entity.getComponent().getId();
                         final Revision newGroupRevision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), newGroupId);
+
+                        // We don't want the Process Group's position to be updated because we want to keep the position where the user
+                        // placed the Process Group. However, we do want to use the name of the Process Group that is in the Flow Contents.
+                        // To accomplish this, we call updateProcessGroupContents() passing 'true' for the updateSettings flag but null out the position.
+                        flowSnapshot.getFlowContents().setPosition(null);
                         entity = serviceFacade.updateProcessGroupContents(NiFiUserUtils.getNiFiUser(), newGroupRevision, newGroupId,
-                            versionControlInfo, flowSnapshot, getIdGenerationSeed().orElse(null), false, false);
+                        versionControlInfo, flowSnapshot, getIdGenerationSeed().orElse(null), false, true, true);
                     }
 
                     populateRemainingProcessGroupEntityContent(entity);


[04/50] nifi git commit: NIFI-4436: Added additional endpoints; bug fixes

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
new file mode 100644
index 0000000..828b970
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClient.java
@@ -0,0 +1,75 @@
+/*
+ * 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.registry.flow;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class StandardFlowRegistryClient implements FlowRegistryClient {
+    private ConcurrentMap<String, FlowRegistry> registryById = new ConcurrentHashMap<>();
+
+    @Override
+    public FlowRegistry getFlowRegistry(String registryId) {
+        return registryById.get(registryId);
+    }
+
+    @Override
+    public Set<String> getRegistryIdentifiers() {
+        return registryById.keySet();
+    }
+
+    @Override
+    public void addFlowRegistry(final FlowRegistry registry) {
+        final FlowRegistry existing = registryById.putIfAbsent(registry.getIdentifier(), registry);
+        if (existing != null) {
+            throw new IllegalStateException("Cannot add Flow Registry " + registry + " because a Flow Registry already exists with the ID " + registry.getIdentifier());
+        }
+    }
+
+    @Override
+    public FlowRegistry addFlowRegistry(final String registryId, final String registryName, final String registryUrl, final String description) {
+        final URI uri = URI.create(registryUrl);
+        final String uriScheme = uri.getScheme();
+
+        final FlowRegistry registry;
+        if (uriScheme.equalsIgnoreCase("file")) {
+            try {
+                registry = new FileBasedFlowRegistry(registryId, registryUrl);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to create Flow Registry for URI " + registryUrl, e);
+            }
+
+            registry.setName(registryName);
+            registry.setDescription(description);
+        } else {
+            throw new IllegalArgumentException("Cannot create Flow Registry with URI of " + registryUrl
+                + " because there are no known implementations of Flow Registries that can handle URIs of scheme " + uriScheme);
+        }
+
+        addFlowRegistry(registry);
+        return registry;
+    }
+
+    @Override
+    public FlowRegistry removeFlowRegistry(final String registryId) {
+        return registryById.remove(registryId);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
index e3edc30..a75d112 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/registry/flow/mapping/NiFiRegistryFlowMapper.java
@@ -28,10 +28,12 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
@@ -46,6 +48,7 @@ import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.registry.VariableDescriptor;
 import org.apache.nifi.registry.flow.BatchSize;
 import org.apache.nifi.registry.flow.Bundle;
 import org.apache.nifi.registry.flow.ComponentType;
@@ -56,13 +59,14 @@ import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.PortType;
 import org.apache.nifi.registry.flow.Position;
-import org.apache.nifi.registry.flow.RemoteFlowCoordinates;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedConnection;
 import org.apache.nifi.registry.flow.VersionedControllerService;
 import org.apache.nifi.registry.flow.VersionedFunnel;
 import org.apache.nifi.registry.flow.VersionedLabel;
 import org.apache.nifi.registry.flow.VersionedPort;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.VersionedProcessor;
 import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
@@ -78,9 +82,91 @@ public class NiFiRegistryFlowMapper {
 
     public InstantiatedVersionedProcessGroup mapProcessGroup(final ProcessGroup group, final FlowRegistryClient registryClient) {
         versionedComponentIds.clear();
-        return mapGroup(group, registryClient, true);
+        final InstantiatedVersionedProcessGroup mapped = mapGroup(group, registryClient, true);
+
+        // TODO: Test that this works properly
+        populateReferencedAncestorServices(group, mapped);
+
+        // TODO: Test that this works properly
+        populateReferencedAncestorVariables(group, mapped);
+
+        return mapped;
+    }
+
+    private void populateReferencedAncestorServices(final ProcessGroup group, final VersionedProcessGroup versionedGroup) {
+        final Set<ControllerServiceNode> ancestorControllerServices = group.getControllerServices(true);
+        ancestorControllerServices.remove(group.getControllerServices(false));
+        final Map<String, ControllerServiceNode> ancestorServicesById = ancestorControllerServices.stream()
+            .collect(Collectors.toMap(ControllerServiceNode::getIdentifier, Function.identity()));
+
+        final Set<ControllerServiceNode> referenced = new HashSet<>();
+
+        for (final ProcessorNode processor : group.findAllProcessors()) {
+            findReferencedServices(processor, ancestorServicesById, referenced);
+        }
+
+        for (final ControllerServiceNode service : group.findAllControllerServices()) {
+            findReferencedServices(service, ancestorServicesById, referenced);
+        }
+
+        final Set<VersionedControllerService> versionedServices = referenced.stream().map(this::mapControllerService)
+            .collect(Collectors.toCollection(LinkedHashSet::new));
+
+        versionedGroup.getControllerServices().addAll(versionedServices);
+    }
+
+    private Set<ControllerServiceNode> findReferencedServices(final ConfiguredComponent component, final Map<String, ControllerServiceNode> ancestorServicesById,
+        final Set<ControllerServiceNode> referenced) {
+
+        for (final Map.Entry<PropertyDescriptor, String> entry : component.getProperties().entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+            if (descriptor.getControllerServiceDefinition() != null) {
+                final String serviceId = entry.getValue();
+                final ControllerServiceNode serviceNode = ancestorServicesById.get(serviceId);
+                if (serviceNode != null) {
+                    referenced.add(serviceNode);
+                    referenced.addAll(findReferencedServices(serviceNode, ancestorServicesById, referenced));
+                }
+            }
+        }
+
+        return referenced;
+    }
+
+    private void populateReferencedAncestorVariables(final ProcessGroup group, final VersionedProcessGroup versionedGroup) {
+        final Set<String> ancestorVariableNames = new HashSet<>();
+        populateVariableNames(group.getParent(), ancestorVariableNames);
+
+        final Map<String, String> implicitlyDefinedVariables = new HashMap<>();
+        for (final String variableName : ancestorVariableNames) {
+            final boolean isReferenced = !group.getComponentsAffectedByVariable(variableName).isEmpty();
+            if (isReferenced) {
+                final String value = group.getVariableRegistry().getVariableValue(variableName);
+                implicitlyDefinedVariables.put(variableName, value);
+            }
+        }
+
+        if (!implicitlyDefinedVariables.isEmpty()) {
+            // Merge the implicit variables with the explicitly defined variables for the Process Group
+            // and set those as the Versioned Group's variables.
+            implicitlyDefinedVariables.putAll(versionedGroup.getVariables());
+            versionedGroup.setVariables(implicitlyDefinedVariables);
+        }
+    }
+
+    private void populateVariableNames(final ProcessGroup group, final Set<String> variableNames) {
+        if (group == null) {
+            return;
+        }
+
+        group.getVariableRegistry().getVariableMap().keySet().stream()
+            .map(VariableDescriptor::getName)
+            .forEach(variableNames::add);
+
+        populateVariableNames(group.getParent(), variableNames);
     }
 
+
     private InstantiatedVersionedProcessGroup mapGroup(final ProcessGroup group, final FlowRegistryClient registryClient, final boolean topLevel) {
         final InstantiatedVersionedProcessGroup versionedGroup = new InstantiatedVersionedProcessGroup(group.getIdentifier(), group.getProcessGroupIdentifier());
         versionedGroup.setIdentifier(getId(group.getVersionedComponentId(), group.getIdentifier()));
@@ -95,7 +181,7 @@ public class NiFiRegistryFlowMapper {
         if (!topLevel) {
             final VersionControlInformation versionControlInfo = group.getVersionControlInformation();
             if (versionControlInfo != null) {
-                final RemoteFlowCoordinates coordinates = new RemoteFlowCoordinates();
+                final VersionedFlowCoordinates coordinates = new VersionedFlowCoordinates();
                 final String registryId = versionControlInfo.getRegistryIdentifier();
                 final FlowRegistry registry = registryClient.getFlowRegistry(registryId);
                 if (registry == null) {
@@ -237,6 +323,7 @@ public class NiFiRegistryFlowMapper {
     private Map<String, String> mapProperties(final ConfiguredComponent component) {
         final Map<String, String> mapped = new HashMap<>();
         component.getProperties().keySet().stream()
+            .filter(property -> !property.isSensitive())
             .forEach(property -> {
                 String value = component.getProperty(property);
                 if (value == null) {
@@ -312,7 +399,7 @@ public class NiFiRegistryFlowMapper {
         versionedPort.setConcurrentlySchedulableTaskCount(port.getMaxConcurrentTasks());
         versionedPort.setName(port.getName());
         versionedPort.setPosition(mapPosition(port.getPosition()));
-        versionedPort.setType(PortType.valueOf(port.getComponentType()));
+        versionedPort.setType(PortType.valueOf(port.getConnectableType().name()));
         return versionedPort;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/BundleUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/BundleUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/BundleUtils.java
index eda045a..807691f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/BundleUtils.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/util/BundleUtils.java
@@ -19,6 +19,7 @@ package org.apache.nifi.util;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.web.api.dto.BundleDTO;
 
 import java.util.List;
@@ -28,7 +29,6 @@ import java.util.stream.Collectors;
  * Utility class for Bundles.
  */
 public final class BundleUtils {
-
     private static BundleCoordinate findBundleForType(final String type, final BundleCoordinate desiredCoordinate) {
         final List<Bundle> bundles = ExtensionManager.getBundles(type);
         if (bundles.isEmpty()) {
@@ -140,4 +140,50 @@ public final class BundleUtils {
         }
     }
 
+
+    /**
+     * Discovers the compatible bundle details for the components in the specified Versioned Process Group and updates the Versioned Process Group
+     * to reflect the appropriate bundles.
+     *
+     * @param versionedGroup the versioned group
+     */
+    public static void discoverCompatibleBundles(final VersionedProcessGroup versionedGroup) {
+        if (versionedGroup.getProcessors() != null) {
+            versionedGroup.getProcessors().forEach(processor -> {
+                final BundleCoordinate coordinate = BundleUtils.getCompatibleBundle(processor.getType(), createBundleDto(processor.getBundle()));
+
+                final org.apache.nifi.registry.flow.Bundle bundle = new org.apache.nifi.registry.flow.Bundle();
+                bundle.setArtifact(coordinate.getId());
+                bundle.setGroup(coordinate.getGroup());
+                bundle.setVersion(coordinate.getVersion());
+                processor.setBundle(bundle);
+            });
+        }
+
+        if (versionedGroup.getControllerServices() != null) {
+            versionedGroup.getControllerServices().forEach(controllerService -> {
+                final BundleCoordinate coordinate = BundleUtils.getCompatibleBundle(controllerService.getType(), createBundleDto(controllerService.getBundle()));
+
+                final org.apache.nifi.registry.flow.Bundle bundle = new org.apache.nifi.registry.flow.Bundle();
+                bundle.setArtifact(coordinate.getId());
+                bundle.setGroup(coordinate.getGroup());
+                bundle.setVersion(coordinate.getVersion());
+                controllerService.setBundle(bundle);
+            });
+        }
+
+        if (versionedGroup.getProcessGroups() != null) {
+            versionedGroup.getProcessGroups().forEach(processGroup -> {
+                discoverCompatibleBundles(processGroup);
+            });
+        }
+    }
+
+    public static BundleDTO createBundleDto(final org.apache.nifi.registry.flow.Bundle bundle) {
+        final BundleDTO dto = new BundleDTO();
+        dto.setArtifact(bundle.getArtifact());
+        dto.setGroup(dto.getGroup());
+        dto.setVersion(dto.getVersion());
+        return dto;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
index 8186c8b..8954f39 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
@@ -26,6 +26,8 @@
                 </xs:sequence>
             </xs:choice>
 
+            <xs:element name="registries" type="RegistriesType" minOccurs="0" maxOccurs="1" />
+
             <!-- Groupings of Processors/Ports -->
             <xs:element name="rootGroup" type="RootProcessGroupType" />
             
@@ -38,6 +40,21 @@
         <xs:attribute name="encoding-version" type="xs:string"/>
     </xs:complexType>
 	
+    <xs:complexType name="RegistriesType">
+        <xs:sequence>
+            <xs:element name="flowRegistry" type="FlowRegistryType" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+    
+    <xs:complexType name="FlowRegistryType">
+        <xs:sequence>
+            <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="name" type="NonEmptyStringType" />
+            <xs:element name="url" type="NonEmptyStringType" />
+            <xs:element name="description" type="NonEmptyStringType" />
+        </xs:sequence>
+    </xs:complexType>
+    
     <!-- the processor "id" is a key that should be valid within each flowController-->
     <xs:complexType name="ProcessorType">
         <xs:sequence>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
index 6a3ec8b..fc42c62 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/nifi-context.xml
@@ -36,9 +36,7 @@
     </bean>
 
     <!-- flow registry -->
-    <bean id="flowRegistryClient" class="org.apache.nifi.registry.flow.FileBasedFlowRegistryClient">
-        <constructor-arg index="0" type="java.io.File" value="../flowRegistry" />
-    </bean>
+    <bean id="flowRegistryClient" class="org.apache.nifi.registry.flow.StandardFlowRegistryClient" />
 
     <!-- flow controller -->
     <bean id="flowController" class="org.apache.nifi.spring.FlowControllerFactoryBean">

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
index 18dc51b..db4ac59 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/service/mock/MockProcessGroup.java
@@ -654,7 +654,7 @@ public class MockProcessGroup implements ProcessGroup {
     }
 
     @Override
-    public void updateFlow(VersionedFlowSnapshot proposedFlow, String componentIdSeed, boolean verifyNotDirty) {
+    public void updateFlow(VersionedFlowSnapshot proposedFlow, String componentIdSeed, boolean verifyNotDirty, boolean updateSettings) {
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
index ca3e95f..31f1fbe 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/fingerprint/FingerprintFactoryTest.java
@@ -32,6 +32,8 @@ import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
+import java.util.Optional;
+
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.connectable.Position;
@@ -208,9 +210,11 @@ public class FingerprintFactoryTest {
         when(component.getProxyPort()).thenReturn(null);
         when(component.getProxyUser()).thenReturn(null);
         when(component.getProxyPassword()).thenReturn(null);
+        when(component.getVersionedComponentId()).thenReturn(Optional.empty());
 
         // Assert fingerprints with expected one.
         final String expected = "id" +
+                "NO_VALUE" +
                 "http://node1:8080/nifi, http://node2:8080/nifi" +
                 "eth0" +
                 "10 sec" +
@@ -245,9 +249,11 @@ public class FingerprintFactoryTest {
         when(component.getProxyPort()).thenReturn(3128);
         when(component.getProxyUser()).thenReturn("proxy-user");
         when(component.getProxyPassword()).thenReturn("proxy-pass");
+        when(component.getVersionedComponentId()).thenReturn(Optional.empty());
 
         // Assert fingerprints with expected one.
         final String expected = "id" +
+                "NO_VALUE" +
                 "http://node1:8080/nifi, http://node2:8080/nifi" +
                 "NO_VALUE" +
                 "10 sec" +
@@ -273,6 +279,7 @@ public class FingerprintFactoryTest {
         when(groupComponent.getPosition()).thenReturn(new Position(10.5, 20.3));
         when(groupComponent.getTargetUri()).thenReturn("http://node1:8080/nifi");
         when(groupComponent.getTransportProtocol()).thenReturn(SiteToSiteTransportProtocol.RAW);
+        when(groupComponent.getVersionedComponentId()).thenReturn(Optional.empty());
 
         final RemoteGroupPort portComponent = mock(RemoteGroupPort.class);
         when(groupComponent.getInputPorts()).thenReturn(Collections.singleton(portComponent));
@@ -288,6 +295,7 @@ public class FingerprintFactoryTest {
         when(portComponent.getBatchDuration()).thenReturn("10sec");
         // Serializer doesn't serialize if a port doesn't have any connection.
         when(portComponent.hasIncomingConnection()).thenReturn(true);
+        when(portComponent.getVersionedComponentId()).thenReturn(Optional.empty());
 
         // Assert fingerprints with expected one.
         final String expected = "portId" +

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryUtils.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryUtils.java
new file mode 100644
index 0000000..b1da06a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/flow/FlowRegistryUtils.java
@@ -0,0 +1,74 @@
+/*
+ * 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.registry.flow;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.util.Tuple;
+import org.apache.nifi.web.NiFiCoreException;
+import org.apache.nifi.web.api.dto.BundleDTO;
+
+public class FlowRegistryUtils {
+
+    public static boolean containsRestrictedComponent(final VersionedProcessGroup group) {
+        final Set<Tuple<String, BundleCoordinate>> componentTypes = new HashSet<>();
+        populateComponentTypes(group, componentTypes);
+
+        for (final Tuple<String, BundleCoordinate> tuple : componentTypes) {
+            final ConfigurableComponent component = ExtensionManager.getTempComponent(tuple.getKey(), tuple.getValue());
+            if (component == null) {
+                throw new NiFiCoreException("Could not create an instance of component " + tuple.getKey() + " using bundle coordinates " + tuple.getValue());
+            }
+
+            final boolean isRestricted = component.getClass().isAnnotationPresent(Restricted.class);
+            if (isRestricted) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static void populateComponentTypes(final VersionedProcessGroup group, final Set<Tuple<String, BundleCoordinate>> componentTypes) {
+        group.getProcessors().stream()
+            .map(versionedProc -> new Tuple<>(versionedProc.getType(), createBundleCoordinate(versionedProc.getBundle())))
+            .forEach(componentTypes::add);
+
+        group.getControllerServices().stream()
+            .map(versionedSvc -> new Tuple<>(versionedSvc.getType(), createBundleCoordinate(versionedSvc.getBundle())))
+            .forEach(componentTypes::add);
+
+        for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
+            populateComponentTypes(childGroup, componentTypes);
+        }
+    }
+
+
+    public static BundleCoordinate createBundleCoordinate(final Bundle bundle) {
+        return new BundleCoordinate(bundle.getGroup(), bundle.getArtifact(), bundle.getVersion());
+    }
+
+    public static BundleDTO createBundleDto(final Bundle bundle) {
+        return new BundleDTO(bundle.getGroup(), bundle.getArtifact(), bundle.getVersion());
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index a68ad0c..d851677 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -407,6 +407,13 @@ public interface NiFiServiceFacade {
     void verifyComponentTypes(FlowSnippetDTO snippet);
 
     /**
+     * Verifies the types of components in a versioned process group
+     *
+     * @param versionedGroup the proposed process group
+     */
+    void verifyComponentTypes(VersionedProcessGroup versionedGroup);
+
+    /**
      * Creates a new Template based off the specified snippet.
      *
      * @param name name
@@ -1385,10 +1392,11 @@ public interface NiFiServiceFacade {
      * @param versionControlInfo the Version Control information
      * @param snapshot the new snapshot
      * @param componentIdSeed the seed to use for generating new component ID's
+     * @param updateSettings whether or not the process group's name and position should be updated
      * @return the Process Group
      */
-    ProcessGroupEntity updateProcessGroup(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
-        boolean verifyNotModified);
+    ProcessGroupEntity updateProcessGroupContents(NiFiUser user, Revision revision, String groupId, VersionControlInformationDTO versionControlInfo, VersionedFlowSnapshot snapshot, String componentIdSeed,
+        boolean verifyNotModified, boolean updateSettings);
 
     // ----------------------------------------
     // Component state methods

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 0105bf1..2b5b5c3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -16,7 +16,32 @@
  */
 package org.apache.nifi.web;
 
-import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.Component;
 import org.apache.nifi.action.FlowChangeAction;
@@ -58,6 +83,7 @@ import org.apache.nifi.components.ValidationResult;
 import org.apache.nifi.components.Validator;
 import org.apache.nifi.components.state.Scope;
 import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
@@ -85,10 +111,9 @@ import org.apache.nifi.history.History;
 import org.apache.nifi.history.HistoryQuery;
 import org.apache.nifi.history.PreviousValue;
 import org.apache.nifi.registry.ComponentVariableRegistry;
-import org.apache.nifi.registry.flow.ConnectableComponent;
 import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
-import org.apache.nifi.registry.flow.RemoteFlowCoordinates;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
 import org.apache.nifi.registry.flow.UnknownResourceException;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.VersionedComponent;
@@ -103,20 +128,19 @@ import org.apache.nifi.registry.flow.diff.FlowComparison;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
 import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
-import org.apache.nifi.registry.flow.mapping.InstantiatedConnectableComponent;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteGroupPort;
 import org.apache.nifi.registry.flow.mapping.NiFiRegistryFlowMapper;
+import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.reporting.Bulletin;
 import org.apache.nifi.reporting.BulletinQuery;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.reporting.ComponentType;
 import org.apache.nifi.util.NiFiProperties;
-import org.apache.nifi.util.Tuple;
 import org.apache.nifi.web.api.dto.AccessPolicyDTO;
 import org.apache.nifi.web.api.dto.AccessPolicySummaryDTO;
 import org.apache.nifi.web.api.dto.AffectedComponentDTO;
@@ -239,6 +263,7 @@ import org.apache.nifi.web.dao.LabelDAO;
 import org.apache.nifi.web.dao.PortDAO;
 import org.apache.nifi.web.dao.ProcessGroupDAO;
 import org.apache.nifi.web.dao.ProcessorDAO;
+import org.apache.nifi.web.dao.RegistryDAO;
 import org.apache.nifi.web.dao.RemoteProcessGroupDAO;
 import org.apache.nifi.web.dao.ReportingTaskDAO;
 import org.apache.nifi.web.dao.SnippetDAO;
@@ -257,30 +282,7 @@ import org.apache.nifi.web.util.SnippetUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Response;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
+import com.google.common.collect.Sets;
 
 /**
  * Implementation of NiFiServiceFacade that performs revision checking.
@@ -312,6 +314,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     private UserDAO userDAO;
     private UserGroupDAO userGroupDAO;
     private AccessPolicyDAO accessPolicyDAO;
+    private RegistryDAO registryDAO;
     private ClusterCoordinator clusterCoordinator;
     private HeartbeatMonitor heartbeatMonitor;
     private LeaderElectionManager leaderElectionManager;
@@ -331,8 +334,6 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
     private AuthorizableLookup authorizableLookup;
 
-    private Map<String, Tuple<Revision, RegistryDTO>> registryCache = new HashMap<>();
-
     // -----------------------------------------
     // Synchronization methods
     // -----------------------------------------
@@ -1849,6 +1850,11 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
+    public void verifyComponentTypes(final VersionedProcessGroup versionedGroup) {
+        controllerFacade.verifyComponentTypes(versionedGroup);
+    }
+
+    @Override
     public TemplateDTO createTemplate(final String name, final String description, final String snippetId, final String groupId, final Optional<String> idGenerationSeed) {
         // get the specified snippet
         final Snippet snippet = snippetDAO.getSnippet(snippetId);
@@ -2260,44 +2266,101 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return entityFactory.createControllerServiceEntity(snapshot, null, permissions, null);
     }
 
-    private RegistryEntity createRegistryEntity(final Revision updatedRevision, final RegistryDTO registryDTO) {
-        final RegistryEntity entity = new RegistryEntity();
-        entity.setId(registryDTO.getId());
-        entity.setPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getController()));
-        entity.setRevision(dtoFactory.createRevisionDTO(updatedRevision));
-        entity.setComponent(registryDTO);
-        return entity;
-    }
 
     @Override
     public RegistryEntity createRegistry(Revision revision, RegistryDTO registryDTO) {
-        registryCache.put(registryDTO.getId(), new Tuple(revision, registryDTO));
-        return createRegistryEntity(revision, registryDTO);
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // read lock on the containing group
+        // request claim for component to be created... revision already verified (version == 0)
+        final RevisionClaim claim = new StandardRevisionClaim(revision);
+
+        // update revision through revision manager
+        final RevisionUpdate<FlowRegistry> revisionUpdate = revisionManager.updateRevision(claim, user, () -> {
+            // add the component
+            final FlowRegistry registry = registryDAO.createFlowRegistry(registryDTO);
+
+            // save the flow
+            controllerFacade.save();
+
+            final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
+            return new StandardRevisionUpdate<>(registry, lastMod);
+        });
+
+        final FlowRegistry registry = revisionUpdate.getComponent();
+        return createRegistryEntity(registry);
     }
 
     @Override
-    public RegistryEntity getRegistry(String registryId) {
-        final Tuple<Revision, RegistryDTO> registry = registryCache.get(registryId);
-        return createRegistry(registry.getKey(), registry.getValue());
+    public RegistryEntity getRegistry(final String registryId) {
+        final FlowRegistry registry = registryDAO.getFlowRegistry(registryId);
+        return createRegistryEntity(registry);
+    }
+
+    private RegistryEntity createRegistryEntity(final FlowRegistry flowRegistry) {
+        if (flowRegistry == null) {
+            return null;
+        }
+
+        final RegistryDTO dto = dtoFactory.createRegistryDto(flowRegistry);
+        final Revision revision = revisionManager.getRevision(dto.getId());
+
+        final RegistryEntity entity = new RegistryEntity();
+        entity.setComponent(dto);
+        entity.setRevision(dtoFactory.createRevisionDTO(revision));
+        entity.setId(dto.getId());
+
+        // User who created it can read/write it.
+        final PermissionsDTO permissions = new PermissionsDTO();
+        permissions.setCanRead(true);
+        permissions.setCanWrite(true);
+        entity.setPermissions(permissions);
+
+        return entity;
     }
 
     @Override
     public Set<RegistryEntity> getRegistries() {
-        return registryCache.values().stream()
-                .map(registry -> createRegistry(registry.getKey(), registry.getValue()))
-                .collect(Collectors.toSet());
+        return registryDAO.getFlowRegistries().stream()
+            .map(this::createRegistryEntity)
+            .collect(Collectors.toSet());
     }
 
     @Override
     public RegistryEntity updateRegistry(Revision revision, RegistryDTO registryDTO) {
-        registryCache.put(registryDTO.getId(), new Tuple(revision, registryDTO));
-        return createRegistryEntity(revision, registryDTO);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        final FlowRegistry registry = registryDAO.getFlowRegistry(registryDTO.getId());
+        final RevisionUpdate<FlowRegistry> revisionUpdate = revisionManager.updateRevision(revisionClaim, user, () -> {
+            registry.setDescription(registryDTO.getDescription());
+            registry.setName(registryDTO.getName());
+            registry.setURL(registryDTO.getUri());
+
+            controllerFacade.save();
+
+            final Revision updatedRevision = revisionManager.getRevision(revision.getComponentId()).incrementRevision(revision.getClientId());
+            final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity());
+
+            return new StandardRevisionUpdate<FlowRegistry>(registry, lastModification);
+        });
+
+        final FlowRegistry updatedReg = revisionUpdate.getComponent();
+        return createRegistryEntity(updatedReg);
     }
 
     @Override
-    public RegistryEntity deleteRegistry(Revision revision, String registryId) {
-        final Tuple<Revision, RegistryDTO> registry = registryCache.remove(registryId);
-        return createRegistryEntity(registry.getKey(), registry.getValue());
+    public RegistryEntity deleteRegistry(final Revision revision, final String registryId) {
+        final RevisionClaim claim = new StandardRevisionClaim(revision);
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        final FlowRegistry registry = revisionManager.deleteRevision(claim, user, () -> {
+            final FlowRegistry reg = registryDAO.removeFlowRegistry(registryId);
+            controllerFacade.save();
+            return reg;
+        });
+
+        return createRegistryEntity(registry);
     }
 
     @Override
@@ -3665,6 +3728,10 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             })
             .collect(Collectors.toCollection(HashSet::new));
 
+        final Map<String, List<Connection>> connectionsByVersionedId = group.findAllConnections().stream()
+            .filter(conn -> conn.getVersionedComponentId().isPresent())
+            .collect(Collectors.groupingBy(conn -> conn.getVersionedComponentId().get()));
+
         for (final FlowDifference difference : comparison.getDifferences()) {
             VersionedComponent component = difference.getComponentA();
             if (component == null) {
@@ -3674,58 +3741,47 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             if (component.getComponentType() == org.apache.nifi.registry.flow.ComponentType.CONNECTION) {
                 final VersionedConnection connection = (VersionedConnection) component;
 
-                final ConnectableComponent source = connection.getSource();
-                final ConnectableComponent destination = connection.getDestination();
+                final String versionedConnectionId = connection.getIdentifier();
+                final List<Connection> instances = connectionsByVersionedId.get(versionedConnectionId);
+                if (instances == null) {
+                    continue;
+                }
 
-                affectedComponents.add(createAffectedComponentEntity((InstantiatedConnectableComponent) source, user));
-                affectedComponents.add(createAffectedComponentEntity((InstantiatedConnectableComponent) destination, user));
+                for (final Connection instance : instances) {
+                    affectedComponents.add(createAffectedComponentEntity(instance.getSource(), user));
+                    affectedComponents.add(createAffectedComponentEntity(instance.getDestination(), user));
+                }
             }
         }
 
         return affectedComponents;
     }
 
-    private String getComponentState(final InstantiatedConnectableComponent localComponent) {
-        final String componentId = localComponent.getInstanceId();
-        final String groupId = localComponent.getInstanceGroupId();
 
-        switch (localComponent.getType()) {
-            case PROCESSOR:
-                return processorDAO.getProcessor(componentId).getPhysicalScheduledState().name();
-            case REMOTE_INPUT_PORT:
-                return remoteProcessGroupDAO.getRemoteProcessGroup(groupId).getInputPort(componentId).getScheduledState().name();
-            case REMOTE_OUTPUT_PORT:
-                return remoteProcessGroupDAO.getRemoteProcessGroup(groupId).getOutputPort(componentId).getScheduledState().name();
-            default:
-                return null;
-        }
-    }
-
-    private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedVersionedComponent instance, final String componentTypeName, final String componentState, final NiFiUser user) {
+    private AffectedComponentEntity createAffectedComponentEntity(final Connectable connectable, final NiFiUser user) {
         final AffectedComponentEntity entity = new AffectedComponentEntity();
-        entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(instance.getInstanceId())));
-        entity.setId(instance.getInstanceId());
+        entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(connectable.getIdentifier())));
+        entity.setId(connectable.getIdentifier());
 
-        final Authorizable authorizable = getAuthorizable(componentTypeName, instance);
+        final Authorizable authorizable = getAuthorizable(connectable);
         final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable, user);
         entity.setPermissions(permissionsDto);
 
         final AffectedComponentDTO dto = new AffectedComponentDTO();
-        dto.setId(instance.getInstanceId());
-        dto.setReferenceType(componentTypeName);
-        dto.setProcessGroupId(instance.getInstanceGroupId());
-        dto.setState(componentState);
+        dto.setId(connectable.getIdentifier());
+        dto.setReferenceType(connectable.getConnectableType().name());
+        dto.setProcessGroupId(connectable.getProcessGroupIdentifier());
+        dto.setState(connectable.getScheduledState().name());
 
         entity.setComponent(dto);
         return entity;
     }
 
-    private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedConnectableComponent instance, final NiFiUser user) {
+    private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedVersionedComponent instance, final String componentTypeName, final String componentState, final NiFiUser user) {
         final AffectedComponentEntity entity = new AffectedComponentEntity();
         entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(instance.getInstanceId())));
         entity.setId(instance.getInstanceId());
 
-        final String componentTypeName = instance.getType().name();
         final Authorizable authorizable = getAuthorizable(componentTypeName, instance);
         final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable, user);
         entity.setPermissions(permissionsDto);
@@ -3734,12 +3790,24 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         dto.setId(instance.getInstanceId());
         dto.setReferenceType(componentTypeName);
         dto.setProcessGroupId(instance.getInstanceGroupId());
-        dto.setState(getComponentState(instance));
+        dto.setState(componentState);
 
         entity.setComponent(dto);
         return entity;
     }
 
+
+    private Authorizable getAuthorizable(final Connectable connectable) {
+        switch (connectable.getConnectableType()) {
+            case REMOTE_INPUT_PORT:
+            case REMOTE_OUTPUT_PORT:
+                final String rpgId = ((RemoteGroupPort) connectable).getRemoteProcessGroup().getIdentifier();
+                return authorizableLookup.getRemoteProcessGroup(rpgId);
+            default:
+                return authorizableLookup.getLocalConnectable(connectable.getIdentifier());
+        }
+    }
+
     private Authorizable getAuthorizable(final String componentTypeName, final InstantiatedVersionedComponent versionedComponent) {
         final String componentId = versionedComponent.getInstanceId();
 
@@ -3820,7 +3888,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     private void populateVersionedFlows(final VersionedProcessGroup group) throws IOException {
-        final RemoteFlowCoordinates remoteCoordinates = group.getRemoteFlowCoordinates();
+        final VersionedFlowCoordinates remoteCoordinates = group.getVersionedFlowCoordinates();
 
         if (remoteCoordinates != null) {
             final String registryUrl = remoteCoordinates.getRegistryUrl();
@@ -3868,17 +3936,17 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified) {
 
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        return updateProcessGroup(user, revision, groupId, versionControlInfo, proposedFlowSnapshot, componentIdSeed, verifyNotModified);
+        return updateProcessGroupContents(user, revision, groupId, versionControlInfo, proposedFlowSnapshot, componentIdSeed, verifyNotModified, true);
     }
 
     @Override
-    public ProcessGroupEntity updateProcessGroup(final NiFiUser user, final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
-        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified) {
+    public ProcessGroupEntity updateProcessGroupContents(final NiFiUser user, final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
+        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings) {
 
         final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(groupId);
         final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(user, revision,
             processGroupNode,
-            () -> processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified),
+            () -> processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings),
             processGroup -> dtoFactory.createProcessGroupDto(processGroup));
 
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);
@@ -4243,27 +4311,11 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         this.leaderElectionManager = leaderElectionManager;
     }
 
+    public void setRegistryDAO(RegistryDAO registryDao) {
+        this.registryDAO = registryDao;
+    }
+
     public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
         this.flowRegistryClient = flowRegistryClient;
-
-        // temp code to load the registry client cache
-        final Set<String> registryIdentifiers = flowRegistryClient.getRegistryIdentifiers();
-        if (registryIdentifiers != null) {
-
-            for (final String registryIdentifier : registryIdentifiers) {
-                final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(registryIdentifier);
-
-                final RegistryDTO registry = new RegistryDTO();
-                registry.setId(registryIdentifier);
-                registry.setName(flowRegistry.getName());
-                registry.setUri(flowRegistry.getURL());
-                registry.setDescription("Default client for storing Flow Revisions to the local disk.");
-
-                final RegistryEntity registryEntity = new RegistryEntity();
-                registryEntity.setComponent(registry);
-
-                registryCache.put(registryIdentifier, new Tuple(new Revision(0L, null, registryIdentifier), registry));
-            }
-        }
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index ebed0ad..11c548f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -16,12 +16,57 @@
  */
 package org.apache.nifi.web.api;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeAccess;
@@ -42,6 +87,8 @@ import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.serialization.FlowEncodingVersion;
 import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.registry.flow.FlowRegistryUtils;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateStep;
 import org.apache.nifi.remote.util.SiteToSiteRestApiClient;
@@ -64,6 +111,7 @@ import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.dto.flow.FlowDTO;
 import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
@@ -104,55 +152,12 @@ import org.slf4j.LoggerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.stream.XMLStreamReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 
 /**
  * RESTful endpoint for managing a Group.
@@ -826,7 +831,7 @@ public class ProcessGroupResource extends ApplicationResource {
         }
 
         final Map<String, String> headers = new HashMap<>();
-        final MultivaluedMap<String, String> requestEntity = new MultivaluedHashMap();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedHashMap<>();
 
         boolean continuePolling = true;
         while (continuePolling) {
@@ -983,7 +988,7 @@ public class ProcessGroupResource extends ApplicationResource {
         }
 
         final Map<String, String> headers = new HashMap<>();
-        final MultivaluedMap<String, String> requestEntity = new MultivaluedHashMap();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedHashMap<>();
 
         boolean continuePolling = true;
         while (continuePolling) {
@@ -1513,9 +1518,10 @@ public class ProcessGroupResource extends ApplicationResource {
      * Adds the specified process group.
      *
      * @param httpServletRequest request
-     * @param groupId            The group id
+     * @param groupId The group id
      * @param requestProcessGroupEntity A processGroupEntity
      * @return A processGroupEntity
+     * @throws IOException if the request indicates that the Process Group should be imported from a Flow Registry and NiFi is unable to communicate with the Flow Registry
      */
     @POST
     @Consumes(MediaType.APPLICATION_JSON)
@@ -1547,7 +1553,7 @@ public class ProcessGroupResource extends ApplicationResource {
             @ApiParam(
                     value = "The process group configuration details.",
                     required = true
-            ) final ProcessGroupEntity requestProcessGroupEntity) {
+        ) final ProcessGroupEntity requestProcessGroupEntity) throws IOException {
 
         if (requestProcessGroupEntity == null || requestProcessGroupEntity.getComponent() == null) {
             throw new IllegalArgumentException("Process group details must be specified.");
@@ -1574,6 +1580,28 @@ public class ProcessGroupResource extends ApplicationResource {
         }
         requestProcessGroupEntity.getComponent().setParentGroupId(groupId);
 
+        // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
+        // Step 2: Retrieve flow from Flow Registry
+        // Step 3: Resolve Bundle info
+        // Step 4: Update contents of the ProcessGroupDTO passed in to include the components that need to be added.
+        // Step 5: If any of the components is a Restricted Component, then we must authorize the user
+        //         for write access to the RestrictedComponents resource
+        // Step 6: Replicate the request or call serviceFacade.updateProcessGroup
+
+        final VersionControlInformationDTO versionControlInfo = requestProcessGroupEntity.getComponent().getVersionControlInformation();
+        if (versionControlInfo != null) {
+            // Step 1: Ensure that user has write permissions to the Process Group. If not, then immediately fail.
+            // Step 2: Retrieve flow from Flow Registry
+            final VersionedFlowSnapshot flowSnapshot = serviceFacade.getVersionedFlowSnapshot(versionControlInfo);
+
+            // Step 3: Resolve Bundle info
+            BundleUtils.discoverCompatibleBundles(flowSnapshot.getFlowContents());
+
+            // Step 4: Update contents of the ProcessGroupDTO passed in to include the components that need to be added.
+            requestProcessGroupEntity.setVersionedFlowSnapshot(flowSnapshot);
+        }
+
+        // Step 6: Replicate the request or call serviceFacade.updateProcessGroup
         if (isReplicateRequest()) {
             return replicate(HttpMethod.POST, requestProcessGroupEntity);
         }
@@ -1584,8 +1612,23 @@ public class ProcessGroupResource extends ApplicationResource {
                 lookup -> {
                     final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
                     processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+
+                    // Step 5: If any of the components is a Restricted Component, then we must authorize the user
+                    // for write access to the RestrictedComponents resource
+                    final VersionedFlowSnapshot versionedFlowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
+                    if (versionedFlowSnapshot != null) {
+                        final boolean containsRestrictedComponent = FlowRegistryUtils.containsRestrictedComponent(versionedFlowSnapshot.getFlowContents());
+                        if (containsRestrictedComponent) {
+                            lookup.getRestrictedComponents().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                        }
+                    }
+                },
+                () -> {
+                    final VersionedFlowSnapshot versionedFlowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
+                    if (versionedFlowSnapshot != null) {
+                        serviceFacade.verifyComponentTypes(versionedFlowSnapshot.getFlowContents());
+                    }
                 },
-                null,
                 processGroupGroupEntity -> {
                     // set the processor id as appropriate
                     processGroupGroupEntity.getComponent().setId(generateUuid());
@@ -1593,6 +1636,16 @@ public class ProcessGroupResource extends ApplicationResource {
                     // create the process group contents
                     final Revision revision = getRevision(processGroupGroupEntity, processGroupGroupEntity.getComponent().getId());
                     final ProcessGroupEntity entity = serviceFacade.createProcessGroup(revision, groupId, processGroupGroupEntity.getComponent());
+
+                    final VersionedFlowSnapshot flowSnapshot = requestProcessGroupEntity.getVersionedFlowSnapshot();
+                    if (flowSnapshot != null) {
+                        final RevisionDTO revisionDto = entity.getRevision();
+                        final String newGroupId = entity.getComponent().getId();
+                        final Revision newGroupRevision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), newGroupId);
+                        serviceFacade.updateProcessGroupContents(NiFiUserUtils.getNiFiUser(), newGroupRevision, newGroupId,
+                            versionControlInfo, flowSnapshot, getIdGenerationSeed().orElse(null), false, false);
+                    }
+
                     populateRemainingProcessGroupEntityContent(entity);
 
                     // generate a 201 created response

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 9fbd5e8..27216a4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,12 +17,39 @@
 
 package org.apache.nifi.web.api;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
-import io.swagger.annotations.Authorization;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.Authorizer;
@@ -30,12 +57,11 @@ import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.authorization.user.NiFiUserUtils;
-import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
-import org.apache.nifi.registry.flow.Bundle;
 import org.apache.nifi.registry.flow.ComponentType;
+import org.apache.nifi.registry.flow.FlowRegistryUtils;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
@@ -48,7 +74,6 @@ import org.apache.nifi.web.api.concurrent.AsynchronousWebRequest;
 import org.apache.nifi.web.api.concurrent.RequestManager;
 import org.apache.nifi.web.api.concurrent.StandardAsynchronousWebRequest;
 import org.apache.nifi.web.api.dto.AffectedComponentDTO;
-import org.apache.nifi.web.api.dto.BundleDTO;
 import org.apache.nifi.web.api.dto.DtoFactory;
 import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
@@ -71,36 +96,12 @@ import org.apache.nifi.web.util.Pause;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
 
 @Path("/versions")
 @Api(value = "/versions", description = "Endpoint for managing version control for a flow")
@@ -163,7 +164,7 @@ public class VersionsResource extends ApplicationResource {
 
 
     @POST
-    @Consumes(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("start-requests")
     @ApiOperation(
@@ -402,10 +403,10 @@ public class VersionsResource extends ApplicationResource {
                 final NodeResponse clusterResponse;
                 try {
                     if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                        clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                        clusterResponse = getRequestReplicator().replicate(HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
                     } else {
                         clusterResponse = getRequestReplicator().forwardToCoordinator(
-                            getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                            getClusterCoordinatorNode(), HttpMethod.POST, createRequestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
                     }
                 } catch (final InterruptedException ie) {
                     Thread.currentThread().interrupt();
@@ -466,10 +467,10 @@ public class VersionsResource extends ApplicationResource {
                 final NodeResponse clusterResponse;
                 try {
                     if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
-                        clusterResponse = getRequestReplicator().replicate(HttpMethod.DELETE, requestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                        clusterResponse = getRequestReplicator().replicate(HttpMethod.DELETE, requestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
                     } else {
                         clusterResponse = getRequestReplicator().forwardToCoordinator(
-                            getClusterCoordinatorNode(), HttpMethod.DELETE, requestUri, null, Collections.emptyMap()).awaitMergedResponse();
+                            getClusterCoordinatorNode(), HttpMethod.DELETE, requestUri, new MultivaluedHashMap<>(), Collections.emptyMap()).awaitMergedResponse();
                     }
                 } catch (final InterruptedException ie) {
                     Thread.currentThread().interrupt();
@@ -942,7 +943,7 @@ public class VersionsResource extends ApplicationResource {
 
         // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
         // the flow snapshot to contain compatible bundles.
-        discoverCompatibleBundles(flowSnapshot.getFlowContents());
+        BundleUtils.discoverCompatibleBundles(flowSnapshot.getFlowContents());
 
         // Step 1: Determine which components will be affected by updating the version
         final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, user);
@@ -956,6 +957,12 @@ public class VersionsResource extends ApplicationResource {
             lookup -> {
                 // Step 2: Verify READ and WRITE permissions for user, for every component affected.
                 authorizeAffectedComponents(lookup, affectedComponents);
+
+                final VersionedProcessGroup groupContents = flowSnapshot.getFlowContents();
+                final boolean containsRestrictedComponents = FlowRegistryUtils.containsRestrictedComponent(groupContents);
+                if (containsRestrictedComponents) {
+                    lookup.getRestrictedComponents().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                }
             },
             () -> {
                 // Step 3: Verify that all components in the snapshot exist on all nodes
@@ -1070,7 +1077,7 @@ public class VersionsResource extends ApplicationResource {
 
         // The flow in the registry may not contain the same versions of components that we have in our flow. As a result, we need to update
         // the flow snapshot to contain compatible bundles.
-        discoverCompatibleBundles(flowSnapshot.getFlowContents());
+        BundleUtils.discoverCompatibleBundles(flowSnapshot.getFlowContents());
 
         // Step 1: Determine which components will be affected by updating the version
         final Set<AffectedComponentEntity> affectedComponents = serviceFacade.getComponentsAffectedByVersionChange(groupId, flowSnapshot, user);
@@ -1084,6 +1091,12 @@ public class VersionsResource extends ApplicationResource {
             lookup -> {
                 // Step 2: Verify READ and WRITE permissions for user, for every component affected.
                 authorizeAffectedComponents(lookup, affectedComponents);
+
+                final VersionedProcessGroup groupContents = flowSnapshot.getFlowContents();
+                final boolean containsRestrictedComponents = FlowRegistryUtils.containsRestrictedComponent(groupContents);
+                if (containsRestrictedComponents) {
+                    lookup.getRestrictedComponents().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+                }
             },
             () -> {
                 // Step 3: Verify that all components in the snapshot exist on all nodes
@@ -1258,7 +1271,7 @@ public class VersionsResource extends ApplicationResource {
             final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
             final Revision revision = new Revision(revisionDto.getVersion(), revisionDto.getClientId(), groupId);
             final VersionControlInformationDTO vci = requestEntity.getVersionControlInformation();
-            serviceFacade.updateProcessGroup(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified);
+            serviceFacade.updateProcessGroupContents(user, revision, groupId, vci, flowSnapshot, idGenerationSeed, verifyNotModified, false);
         }
 
         asyncRequest.setLastUpdated(new Date());
@@ -1384,50 +1397,6 @@ public class VersionsResource extends ApplicationResource {
         this.dtoFactory = dtoFactory;
     }
 
-    private BundleDTO createBundleDto(final Bundle bundle) {
-        final BundleDTO dto = new BundleDTO();
-        dto.setArtifact(bundle.getArtifact());
-        dto.setGroup(dto.getGroup());
-        dto.setVersion(dto.getVersion());
-        return dto;
-    }
-
-    /**
-     * Discovers the compatible bundle details for the components in the specified snippet.
-     *
-     * @param versionedGroup the versioned group
-     */
-    private void discoverCompatibleBundles(final VersionedProcessGroup versionedGroup) {
-        if (versionedGroup.getProcessors() != null) {
-            versionedGroup.getProcessors().forEach(processor -> {
-                final BundleCoordinate coordinate = BundleUtils.getCompatibleBundle(processor.getType(), createBundleDto(processor.getBundle()));
-
-                final Bundle bundle = new Bundle();
-                bundle.setArtifact(coordinate.getId());
-                bundle.setGroup(coordinate.getGroup());
-                bundle.setVersion(coordinate.getVersion());
-                processor.setBundle(bundle);
-            });
-        }
-
-        if (versionedGroup.getControllerServices() != null) {
-            versionedGroup.getControllerServices().forEach(controllerService -> {
-                final BundleCoordinate coordinate = BundleUtils.getCompatibleBundle(controllerService.getType(), createBundleDto(controllerService.getBundle()));
-
-                final Bundle bundle = new Bundle();
-                bundle.setArtifact(coordinate.getId());
-                bundle.setGroup(coordinate.getGroup());
-                bundle.setVersion(coordinate.getVersion());
-                controllerService.setBundle(bundle);
-            });
-        }
-
-        if (versionedGroup.getProcessGroups() != null) {
-            versionedGroup.getProcessGroups().forEach(processGroup -> {
-                discoverCompatibleBundles(processGroup);
-            });
-        }
-    }
 
     private static class ActiveRequest {
         private static final long MAX_REQUEST_LOCK_NANOS = TimeUnit.MINUTES.toNanos(1L);

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index ae3fc56..8e0f0c7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -113,6 +113,7 @@ import org.apache.nifi.provenance.lineage.LineageEdge;
 import org.apache.nifi.provenance.lineage.LineageNode;
 import org.apache.nifi.provenance.lineage.ProvenanceEventLineageNode;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistry;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
@@ -3713,6 +3714,15 @@ public final class DtoFactory {
         return nodeDto;
     }
 
+    public RegistryDTO createRegistryDto(FlowRegistry registry) {
+        final RegistryDTO dto = new RegistryDTO();
+        dto.setDescription(registry.getDescription());
+        dto.setId(registry.getIdentifier());
+        dto.setName(registry.getName());
+        dto.setUri(registry.getURL());
+        return dto;
+    }
+
 
     /* setters */
     public void setControllerServiceProvider(final ControllerServiceProvider controllerServiceProvider) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
index 615f00b..29e5f7d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
@@ -82,6 +82,7 @@ import org.apache.nifi.provenance.search.SearchableField;
 import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableDescriptor;
 import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.remote.RemoteGroupPort;
 import org.apache.nifi.remote.RootGroupPort;
 import org.apache.nifi.reporting.ReportingTask;
@@ -1643,6 +1644,9 @@ public class ControllerFacade implements Authorizable {
         return dto;
     }
 
+    public void verifyComponentTypes(VersionedProcessGroup versionedFlow) {
+        flowController.verifyComponentTypesInSnippet(versionedFlow);
+    }
 
 
     private ComponentSearchResultDTO search(final String searchStr, final ProcessorNode procNode) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index 806979f..650d4b3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -113,10 +113,11 @@ public interface ProcessGroupDAO {
      * @param proposedSnapshot Flow the new version of the flow
      * @param versionControlInformation the new Version Control Information
      * @param componentIdSeed the seed value to use for generating ID's for new components
+     * @param updateSettings whether or not to update the process group's name and position
      * @return the process group
      */
     ProcessGroup updateProcessGroupFlow(String groupId, VersionedFlowSnapshot proposedSnapshot, VersionControlInformationDTO versionControlInformation, String componentIdSeed,
-        boolean verifyNotModified);
+        boolean verifyNotModified, boolean updateSettings);
 
     /**
      * Applies the given Version Control Information to the Process Group

http://git-wip-us.apache.org/repos/asf/nifi/blob/6aa8b5c6/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
new file mode 100644
index 0000000..83b5c6d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/RegistryDAO.java
@@ -0,0 +1,35 @@
+/*
+ * 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.web.dao;
+
+import java.util.Set;
+
+import org.apache.nifi.registry.flow.FlowRegistry;
+import org.apache.nifi.web.api.dto.RegistryDTO;
+
+public interface RegistryDAO {
+
+    FlowRegistry createFlowRegistry(RegistryDTO registryDto);
+
+    FlowRegistry getFlowRegistry(String registryId);
+
+    Set<FlowRegistry> getFlowRegistries();
+
+    FlowRegistry removeFlowRegistry(String registryId);
+
+}


[07/50] nifi git commit: NIFI-4436: - Adding support to save a version of a flow based on a selected Process Group. - Adding support for revert changes back to the most recent version. - Adding support to disconnect from version control. - Moving the ver

Posted by bb...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index d3a11dc..9fbd5e8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -17,8 +17,6 @@
 
 package org.apache.nifi.web.api;
 
-import com.sun.jersey.api.client.ClientResponse.Status;
-import com.sun.jersey.api.core.ResourceContext;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -84,9 +82,9 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -109,8 +107,6 @@ import java.util.stream.Collectors;
 public class VersionsResource extends ApplicationResource {
     private static final Logger logger = LoggerFactory.getLogger(VersionsResource.class);
 
-    @Context
-    private ResourceContext resourceContext;
     private NiFiServiceFacade serviceFacade;
     private Authorizer authorizer;
     private ComponentLifecycle clusterComponentLifecycle;
@@ -128,12 +124,11 @@ public class VersionsResource extends ApplicationResource {
     private ActiveRequest activeRequest = null;
     private final Object activeRequestMonitor = new Object();
 
-
     @GET
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
-    @ApiOperation(value = "Gets the Version Control information for a process group", response = VersionControlInformationEntity.class, authorizations = {
+    @ApiOperation(value = "Gets the Version Control information for a process group", response = VersionControlInformationEntity.class, notes = NON_GUARANTEED_ENDPOINT, authorizations = {
         @Authorization(value = "Read - /process-groups/{uuid}")
     })
     @ApiResponses(value = {
@@ -144,6 +139,7 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
     public Response getVersionInformation(@ApiParam(value = "The process group id.", required = true) @PathParam("id") final String groupId) {
+
         if (isReplicateRequest()) {
             return replicate(HttpMethod.GET);
         }
@@ -155,9 +151,11 @@ public class VersionsResource extends ApplicationResource {
         });
 
         // get the version control information for this process group
-        final VersionControlInformationEntity entity = serviceFacade.getVersionControlInformation(groupId);
+        VersionControlInformationEntity entity = serviceFacade.getVersionControlInformation(groupId);
         if (entity == null) {
-            throw new ResourceNotFoundException("Process Group with ID " + groupId + " is not currently under Version Control");
+            final ProcessGroupEntity processGroup = serviceFacade.getProcessGroup(groupId);
+            entity = new VersionControlInformationEntity();
+            entity.setProcessGroupRevision(processGroup.getRevision());
         }
 
         return generateOkResponse(entity).build();
@@ -168,7 +166,10 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("start-requests")
-    @ApiOperation(value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed", response = VersionControlInformationEntity.class)
+    @ApiOperation(
+            value = "Creates a request so that a Process Group can be placed under Version Control or have its Version Control configuration changed",
+            response = VersionControlInformationEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT)
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -186,6 +187,7 @@ public class VersionsResource extends ApplicationResource {
             serviceFacade,
             /* entity */ null,
             lookup -> {
+                // TODO - pass in PG ID to authorize
             },
             /* verifier */ null,
             requestEntity -> {
@@ -213,9 +215,13 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("start-requests/{id}")
-    @ApiOperation(value = "Updates the request with the given ID", response = VersionControlInformationEntity.class, authorizations = {
-        @Authorization(value = "Write - /process-groups/{uuid}")
-    })
+    @ApiOperation(
+            value = "Updates the request with the given ID",
+            response = VersionControlInformationEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Write - /process-groups/{uuid}")
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -301,7 +307,9 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("start-requests/{id}")
-    @ApiOperation(value = "Deletes the request with the given ID")
+    @ApiOperation(
+            value = "Deletes the request with the given ID",
+            notes = NON_GUARANTEED_ENDPOINT)
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -343,9 +351,13 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
-    @ApiOperation(value = "Begins version controlling the Process Group with the given ID", response = VersionControlInformationEntity.class, authorizations = {
-        @Authorization(value = "Read - /process-groups/{uuid}")
-    })
+    @ApiOperation(
+            value = "Begins version controlling the Process Group with the given ID",
+            response = VersionControlInformationEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Read - /process-groups/{uuid}")
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -545,10 +557,14 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
-    @ApiOperation(value = "Stops version controlling the Process Group with the given ID", response = VersionControlInformationEntity.class, authorizations = {
-        @Authorization(value = "Read - /process-groups/{uuid}"),
-        @Authorization(value = "Write - /process-groups/{uuid}"),
-    })
+    @ApiOperation(
+            value = "Stops version controlling the Process Group with the given ID",
+            response = VersionControlInformationEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Read - /process-groups/{uuid}"),
+                @Authorization(value = "Write - /process-groups/{uuid}"),
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -557,10 +573,14 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
     public Response stopVersionControl(
-        @ApiParam(value = "The version is used to verify the client is working with the latest version of the flow.", required = false) @QueryParam(VERSION) final LongParameter version,
-
-        @ApiParam(value = "If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.", required = false) @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) final ClientIdParameter clientId,
-
+        @ApiParam(
+                value = "The version is used to verify the client is working with the latest version of the flow.",
+                required = false)
+        @QueryParam(VERSION) final LongParameter version,
+        @ApiParam(
+                value = "If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.",
+                required = false)
+        @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) final ClientIdParameter clientId,
         @ApiParam("The process group id.") @PathParam("id") final String groupId) throws IOException {
 
         if (isReplicateRequest()) {
@@ -584,8 +604,8 @@ public class VersionsResource extends ApplicationResource {
                 }
             },
             (revision, groupEntity) -> {
-                // set the version control info to null
-                final VersionControlInformationEntity entity = serviceFacade.setVersionControlInformation(requestRevision, groupId, null, null);
+                // disconnect from version control
+                final VersionControlInformationEntity entity = serviceFacade.deleteVersionControl(requestRevision, groupId);
 
                 // generate the response
                 return generateOkResponse(entity).build();
@@ -597,12 +617,14 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("process-groups/{id}")
-    @ApiOperation(value = "For a Process Group that is already under Version Control, this will update the version of the flow to a different version",
-        response = VersionControlInformationEntity.class,
-        authorizations = {
-            @Authorization(value = "Read - /process-groups/{uuid}"),
-            @Authorization(value = "Write - /process-groups/{uuid}")
-        })
+    @ApiOperation(
+            value = "For a Process Group that is already under Version Control, this will update the version of the flow to a different version",
+            response = VersionControlInformationEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Read - /process-groups/{uuid}"),
+                @Authorization(value = "Write - /process-groups/{uuid}")
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -686,10 +708,12 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("update-requests/{id}")
-    @ApiOperation(value = "Returns the Update Request with the given ID",
-        response = VersionedFlowUpdateRequestEntity.class,
-        authorizations = {
-        })
+    @ApiOperation(
+            value = "Returns the Update Request with the given ID",
+            response = VersionedFlowUpdateRequestEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -705,10 +729,12 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("revert-requests/{id}")
-    @ApiOperation(value = "Returns the Revert Request with the given ID",
-        response = VersionedFlowUpdateRequestEntity.class,
-        authorizations = {
-        })
+    @ApiOperation(
+            value = "Returns the Revert Request with the given ID",
+            response = VersionedFlowUpdateRequestEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -750,8 +776,12 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("update-requests/{id}")
-    @ApiOperation(value = "Deletes the Update Request with the given ID", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
-    })
+    @ApiOperation(
+            value = "Deletes the Update Request with the given ID",
+            response = VersionedFlowUpdateRequestEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -767,8 +797,12 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("revert-requests/{id}")
-    @ApiOperation(value = "Deletes the Revert Request with the given ID", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
-    })
+    @ApiOperation(
+            value = "Deletes the Revert Request with the given ID",
+            response = VersionedFlowUpdateRequestEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -813,11 +847,15 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("update-requests/process-groups/{id}")
-    @ApiOperation(value = "For a Process Group that is already under Version Control, this will initiate the action of changing "
-        + "from a specific version of the flow in the Flow Registry to a different version of the flow.", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
-            @Authorization(value = "Read - /process-groups/{uuid}"),
-            @Authorization(value = "Write - /process-groups/{uuid}")
-        })
+    @ApiOperation(
+            value = "For a Process Group that is already under Version Control, this will initiate the action of changing "
+                    + "from a specific version of the flow in the Flow Registry to a different version of the flow.",
+            response = VersionedFlowUpdateRequestEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Read - /process-groups/{uuid}"),
+                @Authorization(value = "Write - /process-groups/{uuid}")
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -970,12 +1008,16 @@ public class VersionsResource extends ApplicationResource {
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("revert-requests/process-groups/{id}")
-    @ApiOperation(value = "For a Process Group that is already under Version Control, this will initiate the action of reverting "
-        + "any changes that have been made to the Process Group since it was last synchronized with the Flow Registry. This will result in the "
-        + "flow matching the Versioned Flow that exists in the Flow Registry.", response = VersionedFlowUpdateRequestEntity.class, authorizations = {
-            @Authorization(value = "Read - /process-groups/{uuid}"),
-            @Authorization(value = "Write - /process-groups/{uuid}")
-        })
+    @ApiOperation(
+            value = "For a Process Group that is already under Version Control, this will initiate the action of reverting "
+                + "any changes that have been made to the Process Group since it was last synchronized with the Flow Registry. This will result in the "
+                + "flow matching the Versioned Flow that exists in the Flow Registry.",
+            response = VersionedFlowUpdateRequestEntity.class,
+            notes = NON_GUARANTEED_ENDPOINT,
+            authorizations = {
+                @Authorization(value = "Read - /process-groups/{uuid}"),
+                @Authorization(value = "Write - /process-groups/{uuid}")
+            })
     @ApiResponses(value = {
         @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
         @ApiResponse(code = 401, message = "Client could not be authenticated."),
@@ -1250,7 +1292,7 @@ public class VersionsResource extends ApplicationResource {
     private <T> T getResponseEntity(final NodeResponse nodeResponse, final Class<T> clazz) {
         T entity = (T) nodeResponse.getUpdatedEntity();
         if (entity == null) {
-            entity = nodeResponse.getClientResponse().getEntity(clazz);
+            entity = nodeResponse.getClientResponse().readEntity(clazz);
         }
         return entity;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
index d09f895..2c14008 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsynchronousWebRequest.java
@@ -17,10 +17,10 @@
 
 package org.apache.nifi.web.api.concurrent;
 
-import java.util.Date;
-
 import org.apache.nifi.authorization.user.NiFiUser;
 
+import java.util.Date;
+
 public interface AsynchronousWebRequest<T> {
 
     /**
@@ -67,7 +67,7 @@ public interface AsynchronousWebRequest<T> {
     /**
      * Indicates the reason that the request failed, or <code>null</code> if the request has not failed
      *
-     * @param explanation the reason that the request failed, or <code>null</code> if the request has not failed
+     * @return the reason that the request failed, or <code>null</code> if the request has not failed
      */
     String getFailureReason();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 489e590..ae3fc56 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,33 +16,7 @@
  */
 package org.apache.nifi.web.api.dto;
 
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.WebApplicationException;
-
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -139,6 +113,7 @@ import org.apache.nifi.provenance.lineage.LineageEdge;
 import org.apache.nifi.provenance.lineage.LineageNode;
 import org.apache.nifi.provenance.lineage.ProvenanceEventLineageNode;
 import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection;
 import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService;
@@ -209,6 +184,32 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
+import javax.ws.rs.WebApplicationException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
 public final class DtoFactory {
 
     @SuppressWarnings("rawtypes")
@@ -225,6 +226,7 @@ public final class DtoFactory {
     private EntityFactory entityFactory;
     private Authorizer authorizer;
     private NiFiProperties properties;
+    private FlowRegistryClient flowRegistryClient;
 
     public ControllerConfigurationDTO createControllerConfigurationDto(final ControllerFacade controllerFacade) {
         final ControllerConfigurationDTO dto = new ControllerConfigurationDTO();
@@ -242,6 +244,7 @@ public final class DtoFactory {
         dto.setSupportsManagedAuthorizer(AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer));
         dto.setSupportsConfigurableUsersAndGroups(AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer));
         dto.setSupportsConfigurableAuthorizer(AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer));
+        dto.setSupportsFlowVersioning(CollectionUtils.isNotEmpty(flowRegistryClient.getRegistryIdentifiers()));
 
         final Date now = new Date();
         dto.setTimeOffset(TimeZone.getDefault().getOffset(now.getTime()));
@@ -1687,6 +1690,9 @@ public final class DtoFactory {
         dto.setId(group.getIdentifier());
         dto.setName(group.getName());
 
+        final VersionControlInformationDTO versionControlInformation = createVersionControlInformationDto(group);
+        dto.setVersionControlInformation(versionControlInformation);
+
         return dto;
     }
 
@@ -2145,7 +2151,7 @@ public final class DtoFactory {
         dto.setComments(group.getComments());
         dto.setName(group.getName());
         dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null));
-        dto.setVersionControlInformation(createVersionControlInformationDto(group.getVersionControlInformation()));
+        dto.setVersionControlInformation(createVersionControlInformationDto(group));
 
         final Map<String, String> variables = group.getVariableRegistry().getVariableMap().entrySet().stream()
             .collect(Collectors.toMap(entry -> entry.getKey().getName(), entry -> entry.getValue()));
@@ -2169,12 +2175,18 @@ public final class DtoFactory {
         return dto;
     }
 
-    public VersionControlInformationDTO createVersionControlInformationDto(final VersionControlInformation versionControlInfo) {
+    public VersionControlInformationDTO createVersionControlInformationDto(final ProcessGroup group) {
+        if (group == null) {
+            return null;
+        }
+
+        final VersionControlInformation versionControlInfo = group.getVersionControlInformation();
         if (versionControlInfo == null) {
             return null;
         }
 
         final VersionControlInformationDTO dto = new VersionControlInformationDTO();
+        dto.setGroupId(group.getIdentifier());
         dto.setRegistryId(versionControlInfo.getRegistryIdentifier());
         dto.setBucketId(versionControlInfo.getBucketIdentifier());
         dto.setFlowId(versionControlInfo.getFlowIdentifier());
@@ -3722,4 +3734,8 @@ public final class DtoFactory {
     public void setProperties(final NiFiProperties properties) {
         this.properties = properties;
     }
+
+    public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
+        this.flowRegistryClient = flowRegistryClient;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index dd8d67f..1e6167c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -213,6 +213,7 @@ public final class EntityFactory {
 
     public ProcessGroupEntity createProcessGroupEntity(final ProcessGroupDTO dto, final RevisionDTO revision, final PermissionsDTO permissions,
                                                        final ProcessGroupStatusDTO status, final List<BulletinEntity> bulletins) {
+
         final ProcessGroupEntity entity = new ProcessGroupEntity();
         entity.setRevision(revision);
         if (dto != null) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index 5f4dba5..806979f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -16,10 +16,6 @@
  */
 package org.apache.nifi.web.dao;
 
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Future;
-
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
@@ -28,6 +24,10 @@ import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+
 public interface ProcessGroupDAO {
 
     /**
@@ -112,7 +112,7 @@ public interface ProcessGroupDAO {
      * @param groupId the ID of the process group
      * @param proposedSnapshot Flow the new version of the flow
      * @param versionControlInformation the new Version Control Information
-     * @param the seed value to use for generating ID's for new components
+     * @param componentIdSeed the seed value to use for generating ID's for new components
      * @return the process group
      */
     ProcessGroup updateProcessGroupFlow(String groupId, VersionedFlowSnapshot proposedSnapshot, VersionControlInformationDTO versionControlInformation, String componentIdSeed,
@@ -128,6 +128,14 @@ public interface ProcessGroupDAO {
     ProcessGroup updateVersionControlInformation(VersionControlInformationDTO versionControlInformation, Map<String, String> versionedComponentMapping);
 
     /**
+     * Disconnects the specified group from version control.
+     *
+     * @param groupId group id
+     * @return the corresponding Process Group
+     */
+    ProcessGroup disconnectVersionControl(String groupId);
+
+    /**
      * Updates the specified variable registry
      *
      * @param variableRegistry the Variable Registry

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 6fa316d..f842a8c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -16,13 +16,6 @@
  */
 package org.apache.nifi.web.dao.impl;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.connectable.Position;
@@ -43,6 +36,13 @@ import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.dao.ProcessGroupDAO;
 
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
 public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGroupDAO {
 
     private FlowController flowController;
@@ -246,6 +246,12 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
         return group;
     }
 
+    public ProcessGroup disconnectVersionControl(final String groupId) {
+        final ProcessGroup group = locateProcessGroup(flowController, groupId);
+        group.disconnectVersionControl();
+        return group;
+    }
+
     @Override
     public ProcessGroup updateProcessGroupFlow(final String groupId, final VersionedFlowSnapshot proposedSnapshot, final VersionControlInformationDTO versionControlInformation,
         final String componentIdSeed, final boolean verifyNotModified) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
index c41d13b..3961be7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClusterReplicationComponentLifecycle.java
@@ -17,19 +17,6 @@
 
 package org.apache.nifi.web.util;
 
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response.Status;
-
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.cluster.coordination.ClusterCoordinator;
 import org.apache.nifi.cluster.coordination.http.replication.RequestReplicator;
@@ -55,7 +42,18 @@ import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.sun.jersey.core.util.MultivaluedMapImpl;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response.Status;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 public class ClusterReplicationComponentLifecycle implements ComponentLifecycle {
     private static final Logger logger = LoggerFactory.getLogger(ClusterReplicationComponentLifecycle.class);
@@ -155,8 +153,9 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
      * Periodically polls the process group with the given ID, waiting for all processors whose ID's are given to have the given Scheduled State.
      *
      * @param user the user making the request
+     * @param originalUri the original uri
      * @param groupId the ID of the Process Group to poll
-     * @param processorIds the ID of all Processors whose state should be equal to the given desired state
+     * @param processors the Processors whose state should be equal to the given desired state
      * @param desiredState the desired state for all processors with the ID's given
      * @param pause the Pause that can be used to wait between polling
      * @return <code>true</code> if successful, <code>false</code> if unable to wait for processors to reach the desired state
@@ -172,7 +171,7 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
         }
 
         final Map<String, String> headers = new HashMap<>();
-        final MultivaluedMap<String, String> requestEntity = new MultivaluedMapImpl();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedHashMap<>();
 
         boolean continuePolling = true;
         while (continuePolling) {
@@ -217,7 +216,7 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
     private <T> T getResponseEntity(final NodeResponse nodeResponse, final Class<T> clazz) {
         T entity = (T) nodeResponse.getUpdatedEntity();
         if (entity == null) {
-            entity = nodeResponse.getClientResponse().getEntity(clazz);
+            entity = nodeResponse.getClientResponse().readEntity(clazz);
         }
         return entity;
     }
@@ -354,7 +353,7 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
         }
 
         final Map<String, String> headers = new HashMap<>();
-        final MultivaluedMap<String, String> requestEntity = new MultivaluedMapImpl();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedHashMap<>();
 
         boolean continuePolling = true;
         while (continuePolling) {
@@ -405,7 +404,7 @@ public class ClusterReplicationComponentLifecycle implements ComponentLifecycle
      * Updates the affected controller services in the specified updateRequest with the serviceEntities.
      *
      * @param serviceEntities service entities
-     * @param updateRequest update request
+     * @param affectedServices affected services
      */
     private void updateAffectedControllerServices(final Set<ControllerServiceEntity> serviceEntities, final Map<String, AffectedComponentEntity> affectedServices) {
         // update the affected components

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
index 22ffec7..e005d28 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/LocalComponentLifecycle.java
@@ -17,12 +17,6 @@
 
 package org.apache.nifi.web.util;
 
-import java.net.URI;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
@@ -38,6 +32,12 @@ import org.apache.nifi.web.revision.RevisionManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
 public class LocalComponentLifecycle implements ComponentLifecycle {
     private static final Logger logger = LoggerFactory.getLogger(LocalComponentLifecycle.class);
 
@@ -232,7 +232,7 @@ public class LocalComponentLifecycle implements ComponentLifecycle {
      * Periodically polls the process group with the given ID, waiting for all controller services whose ID's are given to have the given Controller Service State.
      *
      * @param groupId the ID of the Process Group to poll
-     * @param serviceIds the ID of all Controller Services whose state should be equal to the given desired state
+     * @param affectedServices all Controller Services whose state should be equal to the given desired state
      * @param desiredState the desired state for all services with the ID's given
      * @param pause the Pause that can be used to wait between polling
      * @param user the user that is retrieving the controller services
@@ -275,7 +275,7 @@ public class LocalComponentLifecycle implements ComponentLifecycle {
      * Updates the affected controller services in the specified updateRequest with the serviceEntities.
      *
      * @param serviceEntities service entities
-     * @param updateRequest update request
+     * @param affectedServices all Controller Services whose state should be equal to the given desired state
      */
     private void updateAffectedControllerServices(final Set<ControllerServiceEntity> serviceEntities, final Map<String, AffectedComponentEntity> affectedServices) {
         // update the affected components

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 48db565..99a4f1c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -53,6 +53,7 @@
         <property name="authorizer" ref="authorizer"/>
         <property name="bulletinRepository" ref="bulletinRepository"/>
         <property name="properties" ref="nifiProperties"/>
+        <property name="flowRegistryClient" ref="flowRegistryClient" />
     </bean>
 
     <!-- snippet utils -->
@@ -208,6 +209,7 @@
         <property name="remoteProcessGroupResource" ref="remoteProcessGroupResource"/>
         <property name="connectionResource" ref="connectionResource"/>
         <property name="templateResource" ref="templateResource"/>
+        <property name="controllerResource" ref="controllerResource"/>
         <property name="controllerServiceResource" ref="controllerServiceResource"/>
         <property name="reportingTaskResource" ref="reportingTaskResource"/>
         <property name="processGroupResource" ref="processGroupResource"/>
@@ -215,6 +217,7 @@
         <property name="clusterCoordinator" ref="clusterCoordinator"/>
         <property name="requestReplicator" ref="requestReplicator" />
         <property name="flowController" ref="flowController" />
+        <property name="flowRegistryClient" ref="flowRegistryClient" />
     </bean>
     <bean id="resourceResource" class="org.apache.nifi.web.api.ResourceResource" scope="singleton">
         <property name="serviceFacade" ref="serviceFacade"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
index 4f2cc8e..a6f0385 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
@@ -475,6 +475,7 @@
                                                 <include>${staging.dir}/js/nf/canvas/nf-draggable.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-connectable.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-graph.js</include>
+                                                <include>${staging.dir}/js/nf/canvas/nf-flow-version.js</include>
                                                 <include>${staging.dir}/js/nf/nf-filtered-dialog-common.js</include>
                                                 <include>${staging.dir}/js/nf/nf-status-history.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-queue-listing.js</include>

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
index 413c6c2..40a3b1f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
@@ -57,6 +57,7 @@ nf.canvas.script.tags=<script type="text/javascript" src="js/nf/nf-ng-bridge.js?
 <script type="text/javascript" src="js/nf/canvas/nf-draggable.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-connectable.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-graph.js?${project.version}"></script>\n\
+<script type="text/javascript" src="js/nf/canvas/nf-flow-version.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/nf-filtered-dialog-common.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/nf-status-history.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-queue-listing.js?${project.version}"></script>\n\

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
index 3c7d407..c57b76f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
@@ -115,6 +115,8 @@
         <jsp:include page="/WEB-INF/partials/canvas/instantiate-template-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/fill-color-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/connections-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/save-flow-version-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/registry-configuration-dialog.jsp"/>
         <div id="canvas-container" class="unselectable"></div>
         <div id="canvas-tooltips">
             <div id="processor-tooltips"></div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp
index 13e2146..7dd481f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp
@@ -22,56 +22,56 @@
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.processor}}"
                     id="processor-component"
                     class="component-button icon icon-processor"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.processorComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.inputPort}}"
                     id="port-in-component"
                     class="component-button icon icon-port-in"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.inputPortComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.outputPort}}"
                     id="port-out-component"
                     class="component-button icon icon-port-out"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.outputPortComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.processGroup}}"
                     id="group-component"
                     class="component-button icon icon-group"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.groupComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.remoteProcessGroup}}"
                     id="group-remote-component"
                     class="component-button icon icon-group-remote"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.remoteGroupComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.funnel}}"
                     id="funnel-component"
                     class="component-button icon icon-funnel"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.funnelComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.template}}"
                     id="template-component"
                     class="component-button icon icon-template"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.templateComponent);">
                 <span class="component-button-grip"></span>
             </button>
             <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.label}}"
                     id="label-component"
                     class="component-button icon icon-label"
-                    ng-disabled="!appCtrl.nf.CanvasUtils.canWrite();"
+                    ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();"
                     nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.labelComponent);">
                 <span class="component-button-grip"></span>
             </button>

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
new file mode 100644
index 0000000..7fa90f7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/registry-configuration-dialog.jsp
@@ -0,0 +1,40 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="registry-configuration-dialog" layout="column" class="hidden medium-dialog">
+    <div class="dialog-content">
+        <div class="setting">
+            <div class="setting-name">Name</div>
+            <div class="setting-field">
+                <span id="registry-id" class="hidden"></span>
+                <input type="text" id="registry-name" class="setting-input"/>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Location</div>
+            <div class="setting-field">
+                <input type="text" id="registry-location" class="setting-input"/>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Description</div>
+            <div class="setting-field">
+                <textarea id="registry-description" class="setting-input"></textarea>
+            </div>
+        </div>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
new file mode 100644
index 0000000..dfed409
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/save-flow-version-dialog.jsp
@@ -0,0 +1,54 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="save-flow-version-dialog" layout="column" class="hidden large-dialog">
+    <div class="dialog-content">
+        <div class="setting">
+            <div class="setting-name">Registry</div>
+            <div class="setting-field">
+                <div id="flow-version-registry-combo"></div>
+                <div id="flow-version-registry" class="hidden"></div>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Location</div>
+            <div class="setting-field">
+                <div id="flow-version-bucket-combo"></div>
+                <div id="flow-version-bucket" class="hidden"></div>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Name</div>
+            <div class="setting-field">
+                <span id="flow-version-process-group-id" class="hidden"></span>
+                <input type="text" id="flow-version-name" class="setting-input"/>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Description</div>
+            <div class="setting-field">
+                <textarea id="flow-version-description" class="setting-input"></textarea>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Change Comments</div>
+            <div class="setting-field">
+                <textarea id="flow-version-change-comments" class="setting-input"></textarea>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp
index ca22c04..57f43c6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp
@@ -62,6 +62,9 @@
             <div id="reporting-tasks-tab-content" class="configuration-tab controller-settings-table">
                 <div id="reporting-tasks-table" class="settings-table"></div>
             </div>
+            <div id="registries-tab-content" class="configuration-tab controller-settings-table">
+                <div id="registries-table" class="settings-table"></div>
+            </div>
         </div>
     </div>
     <div id="settings-refresh-container">

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index cb5282b..1c44e71 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -213,6 +213,14 @@ div.progress-label {
 }
 
 /*
+    Flow Version
+ */
+
+#flow-version-description, #flow-version-change-comments {
+    height: 85px;
+}
+
+/*
     Variable Registry
  */
 
@@ -252,6 +260,14 @@ div.slick-cell div.overridden {
 }
 
 /*
+    Registry configuration dialog
+ */
+
+#registry-description {
+    height: 85px;
+}
+
+/*
     General dialog styles.
 */
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
index f5c1a6f..caad5bd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/graph.css
@@ -422,6 +422,14 @@ text.process-group-name {
     font-size: 14px;
 }
 
+text.version-control {
+    font-family: FontAwesome;
+    font-size: 18px;
+    fill: rgba(0, 255, 0, 0.65);
+    stroke: rgba(0, 0, 0, 0.65);
+    visibility: hidden;
+}
+
 text.process-group-contents-count {
     fill: #775351;
     font-size: 15px;

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
index 2439d22..fc930eb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/navigation.css
@@ -278,6 +278,11 @@ rect.birdseye-brush {
     top: 8px;
 }
 
+span.breadcrumb-version-control {
+    color: #0f0;
+    text-shadow: 0px 0px 1px #000;
+}
+
 #breadcrumbs-left-border {
     position: absolute;
     left: 0;

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
index 72cd57b..5bf70b1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
@@ -75,6 +75,21 @@
             },
 
             /**
+             * Updates the version control information for the specified process group.
+             *
+             * @param processGroupId
+             * @param versionControlInformation
+             */
+            updateVersionControlInformation: function (processGroupId, versionControlInformation) {
+                $.each(this.breadcrumbs, function (_, breadcrumbEntity) {
+                    if (breadcrumbEntity.id === processGroupId) {
+                        breadcrumbEntity.breadcrumb.versionControlInformation = versionControlInformation;
+                        return false;
+                    }
+                });
+            },
+
+            /**
              * Reset the breadcrumbs.
              */
             resetBreadcrumbs: function () {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-graph-controls-controller.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-graph-controls-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-graph-controls-controller.js
index e661338..3c9df0b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-graph-controls-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-graph-controls-controller.js
@@ -234,7 +234,7 @@
              */
             getContextName: function () {
                 var selection = nfCanvasUtils.getSelection();
-                var canRead = nfCanvasUtils.canReadFromGroup();
+                var canRead = nfCanvasUtils.canReadCurrentGroup();
 
                 if (selection.empty()) {
                     if (canRead) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
index 90a1140..ff09330 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
@@ -33,6 +33,7 @@
                 'nf.Shell',
                 'nf.VariableRegistry',
                 'nf.ComponentState',
+                'nf.FlowVersion',
                 'nf.Draggable',
                 'nf.Birdseye',
                 'nf.Connection',
@@ -55,8 +56,8 @@
                 'nf.ComponentVersion',
                 'nf.QueueListing',
                 'nf.StatusHistory'],
-            function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
-                return (nf.Actions = factory($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory));
+            function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfFlowVersion, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
+                return (nf.Actions = factory($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfFlowVersion, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.Actions =
@@ -74,6 +75,7 @@
                 require('nf.Shell'),
                 require('nf.VariableRegistry'),
                 require('nf.ComponentState'),
+                require('nf.FlowVersion'),
                 require('nf.Draggable'),
                 require('nf.Birdseye'),
                 require('nf.Connection'),
@@ -111,6 +113,7 @@
             root.nf.Shell,
             root.nf.VariableRegistry,
             root.nf.ComponentState,
+            root.nf.FlowVersion,
             root.nf.Draggable,
             root.nf.Birdseye,
             root.nf.Connection,
@@ -134,7 +137,7 @@
             root.nf.QueueListing,
             root.nf.StatusHistory);
     }
-}(this, function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
+}(this, function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfFlowVersion, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
     'use strict';
 
     var config = {
@@ -1237,6 +1240,51 @@
         },
 
         /**
+         * Shows the flow version dialog.
+         */
+        saveFlowVersion: function (selection) {
+            if (selection.empty()) {
+                nfFlowVersion.showFlowVersionDialog(nfCanvasUtils.getGroupId());
+            } else if (selection.size() === 1) {
+                var selectionData = selection.datum();
+                if (nfCanvasUtils.isProcessGroup(selection)) {
+                    nfFlowVersion.showFlowVersionDialog(selectionData.id);
+                }
+            }
+        },
+
+        /**
+         * Reverts outstanding changes.
+         */
+        revertFlowChanges: function (selection) {
+            if (selection.empty()) {
+                nfFlowVersion.revertFlowChanges(nfCanvasUtils.getGroupId());
+            } else if (selection.size() === 1) {
+                var selectionData = selection.datum();
+                nfFlowVersion.revertFlowChanges(selectionData.id);
+            }
+        },
+
+        /**
+         * Changes the flow version.
+         */
+        changeFlowVersion: function (selection) {
+
+        },
+
+        /**
+         * Disconnects a Process Group from flow versioning.
+         */
+        disconnectFlowVersioning: function (selection) {
+            if (selection.empty()) {
+                nfFlowVersion.disconnectFlowVersioning(nfCanvasUtils.getGroupId());
+            } else if (selection.size() === 1) {
+                var selectionData = selection.datum();
+                nfFlowVersion.disconnectFlowVersioning(selectionData.id);
+            }
+        },
+
+        /**
          * Opens the variable registry for the specified selection of the current group if the selection is emtpy.
          *
          * @param {selection} selection

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
index e3ab5c9..6e17b27 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
@@ -39,6 +39,7 @@
                 'nf.QueueListing',
                 'nf.VariableRegistry',
                 'nf.ComponentState',
+                'nf.FlowVersion',
                 'nf.ComponentVersion',
                 'nf.Draggable',
                 'nf.Connectable',
@@ -82,8 +83,8 @@
                 'nf.ng.Canvas.OperateCtrl',
                 'nf.ng.BreadcrumbsDirective',
                 'nf.ng.DraggableDirective'],
-            function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, gr
 aphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
-                return factory($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComp
 onent, graphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective);
+            function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfFlowVersion, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, lab
 elComponent, graphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
+                return factory($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfFlowVersion, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateCompo
 nent, labelComponent, graphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective);
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = factory(require('jquery'),
@@ -106,6 +107,7 @@
             require('nf.QueueListing'),
             require('nf.VariableRegistry'),
             require('nf.ComponentState'),
+            require('nf.FlowVersion'),
             require('nf.ComponentVersion'),
             require('nf.Draggable'),
             require('nf.Connectable'),
@@ -170,6 +172,7 @@
             root.nf.QueueListing,
             root.nf.VariableRegistry,
             root.nf.ComponentState,
+            root.nf.FlowVersion,
             root.nf.ComponentVersion,
             root.nf.Draggable,
             root.nf.Connectable,
@@ -214,7 +217,7 @@
             root.nf.ng.BreadcrumbsDirective,
             root.nf.ng.DraggableDirective);
     }
-}(this, function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, graphC
 ontrolsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
+}(this, function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfFlowVersion, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelCo
 mponent, graphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
 
     var config = {
         urls: {
@@ -334,6 +337,7 @@
                     nfCanvas.setManagedAuthorizer(configDetails.supportsManagedAuthorizer);
                     nfCanvas.setConfigurableAuthorizer(configDetails.supportsConfigurableAuthorizer);
                     nfCanvas.setConfigurableUsersAndGroups(configDetails.supportsConfigurableUsersAndGroups);
+                    nfCanvas.setSupportsFlowVersioning(configDetails.supportsFlowVersioning);
 
                     // init nfStorage
                     nfStorage.init();
@@ -352,6 +356,7 @@
                     nfQueueListing.init();
                     nfVariableRegistry.init();
                     nfComponentState.init();
+                    nfFlowVersion.init();
                     nfComponentVersion.init(nfSettings);
 
                     // initialize the component behaviors

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
index 68a918a..9506fef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
@@ -1820,6 +1820,13 @@
         },
 
         /**
+         * Returns whether this NiFi supports flow versioning.
+         */
+        supportsFlowVersioning: function () {
+            return nfCanvas.supportsFlowVersioning();
+        },
+
+        /**
          * Returns whether the authorizer is managed.
          */
         isManagedAuthorizer: function () {
@@ -1884,7 +1891,7 @@
          *
          * @returns {boolean}   can write
          */
-        canReadFromGroup: function () {
+        canReadCurrentGroup: function () {
             return nfCanvas.canRead();
         },
 
@@ -1893,7 +1900,7 @@
          *
          * @returns {boolean}   can write
          */
-        canWrite: function () {
+        canWriteCurrentGroup: function () {
             return nfCanvas.canWrite();
         },
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
index 49ded8c..d0ad4ee 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
@@ -85,6 +85,7 @@
     var permissions = null;
     var parentGroupId = null;
     var managedAuthorizer = false;
+    var supportsFlowVersioning = false;
     var configurableAuthorizer = false;
     var configurableUsersAndGroups = false;
     var svg = null;
@@ -909,6 +910,23 @@
         },
 
         /**
+         * Set whether this NiFi supports flow versioning.
+         *
+         * @param bool Whether this NiFi supports flow versioning
+         */
+        setSupportsFlowVersioning: function (bool) {
+            supportsFlowVersioning = bool;
+        },
+
+        /**
+         *
+         * @returns {boolean}
+         */
+        supportsFlowVersioning: function () {
+            return supportsFlowVersioning;
+        },
+
+        /**
          * Set whether the authorizer is configurable.
          *
          * @param bool The boolean value representing whether the authorizer is configurable.


[37/50] nifi git commit: NIFI-4436: - Code clean up. - Improved error handling. - Minor UX improvements. - Adding message to indicate that variables do not support sensitive values. - Preventing a user from changing the flow version to the current versio

Posted by bb...@apache.org.
NIFI-4436:
- Code clean up.
- Improved error handling.
- Minor UX improvements.
- Adding message to indicate that variables do not support sensitive values.
- Preventing a user from changing the flow version to the current version.
- Only presenting buckets a user has appropriate permissions to.
- Adding basic auditing to the version control actions.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/db2cc9fe
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/db2cc9fe
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/db2cc9fe

Branch: refs/heads/master
Commit: db2cc9fec1ca3ad4382f8dee25ce9790ad53c1f7
Parents: 014c542
Author: Matt Gilman <ma...@gmail.com>
Authored: Wed Dec 6 10:47:28 2017 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:55 2018 -0500

----------------------------------------------------------------------
 .../java/org/apache/nifi/action/Operation.java  |  44 ++++---
 .../nifi/admin/dao/impl/StandardActionDAO.java  |   4 +-
 .../nifi/web/api/entity/BucketEntity.java       |  18 +++
 .../nifi/web/api/entity/RegistriesEntity.java   |   4 +-
 .../web/api/entity/RegistryClientEntity.java    |  38 ++++++
 .../web/api/entity/RegistryClientsEntity.java   |  41 +++++++
 .../nifi/web/api/entity/RegistryEntity.java     |  13 +--
 .../apache/nifi/audit/ProcessGroupAuditor.java  | 117 +++++++++++++++----
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  27 ++---
 .../nifi/web/StandardNiFiServiceFacade.java     |  87 ++++++--------
 .../apache/nifi/web/api/ControllerResource.java |  62 +++++-----
 .../org/apache/nifi/web/api/FlowResource.java   |   3 +-
 .../apache/nifi/web/api/VersionsResource.java   |  13 ++-
 .../org/apache/nifi/web/api/dto/DtoFactory.java |   4 +-
 .../apache/nifi/web/api/dto/EntityFactory.java  |  42 +++++++
 .../apache/nifi/web/dao/ProcessGroupDAO.java    |   8 +-
 .../nifi/web/dao/impl/FlowRegistryDAO.java      |   7 +-
 .../web/dao/impl/StandardProcessGroupDAO.java   |  10 +-
 .../partials/canvas/variable-configuration.jsp  |   1 +
 .../main/webapp/WEB-INF/partials/ok-dialog.jsp  |   2 +-
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |   7 ++
 .../webapp/js/jquery/modal/jquery.modal.css     |   9 ++
 .../src/main/webapp/js/nf/canvas/nf-actions.js  |   2 +-
 .../main/webapp/js/nf/canvas/nf-flow-version.js | 110 ++++++++++++-----
 .../webapp/js/nf/canvas/nf-process-group.js     |  32 +++--
 25 files changed, 502 insertions(+), 203 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-api/src/main/java/org/apache/nifi/action/Operation.java
----------------------------------------------------------------------
diff --git a/nifi-api/src/main/java/org/apache/nifi/action/Operation.java b/nifi-api/src/main/java/org/apache/nifi/action/Operation.java
index 0d5072c..99b9495 100644
--- a/nifi-api/src/main/java/org/apache/nifi/action/Operation.java
+++ b/nifi-api/src/main/java/org/apache/nifi/action/Operation.java
@@ -21,18 +21,34 @@ package org.apache.nifi.action;
  */
 public enum Operation {
 
-    Add,
-    Remove,
-    Paste,
-    Configure,
-    Move,
-    Disconnect,
-    Connect,
-    Start,
-    Stop,
-    Enable,
-    Disable,
-    Batch,
-    Purge,
-    ClearState;
+    Add("Add"),
+    Remove("Remove"),
+    Paste("Paste"),
+    Configure("Configure"),
+    Move("Move"),
+    Disconnect("Disconnect"),
+    Connect("Connect"),
+    Start("Start"),
+    Stop("Stop"),
+    Enable("Enable"),
+    Disable("Disable"),
+    Batch("Batch"),
+    Purge("Purge"),
+    ClearState("Clear State"),
+    StartVersionControl("Start Version Control"),
+    StopVersionControl("Stop Version Control"),
+    CommitLocalChanges("Commit Local Changes"),
+    RevertLocalChanges("Revert Local Changes"),
+    ChangeVersion("Change Version");
+
+    private final String label;
+
+    Operation(String label) {
+        this.label = label;
+    }
+
+    @Override
+    public String toString() {
+        return label;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardActionDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardActionDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardActionDAO.java
index df1774c..1b79b18 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardActionDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardActionDAO.java
@@ -219,8 +219,8 @@ public class StandardActionDAO implements ActionDAO {
             statement.setString(1, StringUtils.left(action.getUserIdentity(), 4096));
             statement.setString(2, action.getSourceId());
             statement.setString(3, StringUtils.left(action.getSourceName(), 1000));
-            statement.setString(4, action.getSourceType().toString());
-            statement.setString(5, action.getOperation().toString());
+            statement.setString(4, action.getSourceType().name());
+            statement.setString(5, action.getOperation().name());
             statement.setTimestamp(6, new java.sql.Timestamp(action.getTimestamp().getTime()));
 
             // insert the action

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
index 3d99308..486fdf7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BucketEntity.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.web.api.entity;
 
 import org.apache.nifi.web.api.dto.BucketDTO;
+import org.apache.nifi.web.api.dto.PermissionsDTO;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
@@ -26,8 +27,17 @@ import javax.xml.bind.annotation.XmlRootElement;
 @XmlRootElement(name = "bucketEntity")
 public class BucketEntity extends Entity {
 
+    private String id;
     private BucketDTO bucket;
+    private PermissionsDTO permissions;
 
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
 
     public BucketDTO getBucket() {
         return bucket;
@@ -36,4 +46,12 @@ public class BucketEntity extends Entity {
     public void setBucket(BucketDTO bucket) {
         this.bucket = bucket;
     }
+
+    public PermissionsDTO getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(PermissionsDTO permissions) {
+        this.permissions = permissions;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
index 6705c7a..26754bd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistriesEntity.java
@@ -22,13 +22,13 @@ import java.util.Set;
 /**
  * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a set of RegistryEntity's.
  */
-@XmlRootElement(name = "registriesEntity")
+@XmlRootElement(name = "registryClientsEntity")
 public class RegistriesEntity extends Entity {
 
     private Set<RegistryEntity> registries;
 
     /**
-     * @return collection of LabelEntity's that are being serialized
+     * @return collection of RegistryEntity's that are being serialized
      */
     public Set<RegistryEntity> getRegistries() {
         return registries;

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientEntity.java
new file mode 100644
index 0000000..3b16067
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientEntity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.web.api.entity;
+
+import org.apache.nifi.web.api.dto.RegistryDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a RegistryDTO.
+ */
+@XmlRootElement(name = "registryClientEntity")
+public class RegistryClientEntity extends ComponentEntity {
+
+    private RegistryDTO component;
+
+    public RegistryDTO getComponent() {
+        return component;
+    }
+
+    public void setComponent(RegistryDTO component) {
+        this.component = component;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientsEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientsEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientsEntity.java
new file mode 100644
index 0000000..9d6b849
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryClientsEntity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.web.api.entity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Set;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response to the API. This particular entity holds a reference to a set of RegistryClientEntity's.
+ */
+@XmlRootElement(name = "registryClientsEntity")
+public class RegistryClientsEntity extends Entity {
+
+    private Set<RegistryClientEntity> registries;
+
+    /**
+     * @return collection of RegistryClientEntity's that are being serialized
+     */
+    public Set<RegistryClientEntity> getRegistries() {
+        return registries;
+    }
+
+    public void setRegistries(Set<RegistryClientEntity> registries) {
+        this.registries = registries;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
index 5968579..33ac3b1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/RegistryEntity.java
@@ -24,16 +24,15 @@ import javax.xml.bind.annotation.XmlRootElement;
  * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a RegistryDTO.
  */
 @XmlRootElement(name = "registryEntity")
-public class RegistryEntity extends ComponentEntity {
+public class RegistryEntity {
 
-    private RegistryDTO component;
+    private RegistryDTO registry;
 
-
-    public RegistryDTO getComponent() {
-        return component;
+    public RegistryDTO getRegistry() {
+        return registry;
     }
 
-    public void setComponent(RegistryDTO component) {
-        this.component = component;
+    public void setRegistry(RegistryDTO registry) {
+        this.registry = registry;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/audit/ProcessGroupAuditor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/audit/ProcessGroupAuditor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/audit/ProcessGroupAuditor.java
index 22ad122..363e049 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/audit/ProcessGroupAuditor.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/audit/ProcessGroupAuditor.java
@@ -28,7 +28,10 @@ import org.apache.nifi.authorization.user.NiFiUserUtils;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.VersionControlInformation;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
+import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.dao.ProcessGroupDAO;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
@@ -39,6 +42,7 @@ import org.slf4j.LoggerFactory;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
+import java.util.concurrent.Future;
 
 /**
  * Audits process group creation/removal and configuration changes.
@@ -173,10 +177,13 @@ public class ProcessGroupAuditor extends NiFiAuditor {
      * @throws Throwable ex
      */
     @Around("within(org.apache.nifi.web.dao.ProcessGroupDAO+) && "
-        + "execution(void scheduleComponents(java.lang.String, org.apache.nifi.controller.ScheduledState, java.util.Set)) && "
+        + "execution(java.util.concurrent.Future<Void> scheduleComponents(java.lang.String, org.apache.nifi.controller.ScheduledState, java.util.Set)) && "
         + "args(groupId, state)")
-    public void scheduleComponentsAdvice(ProceedingJoinPoint proceedingJoinPoint, String groupId, ScheduledState state) throws Throwable {
+    public Future<Void> scheduleComponentsAdvice(ProceedingJoinPoint proceedingJoinPoint, String groupId, ScheduledState state) throws Throwable {
         final Operation operation;
+
+        final Future<Void> result = (Future<Void>) proceedingJoinPoint.proceed();
+
         // determine the running state
         if (ScheduledState.RUNNING.equals(state)) {
             operation = Operation.Start;
@@ -184,7 +191,9 @@ public class ProcessGroupAuditor extends NiFiAuditor {
             operation = Operation.Stop;
         }
 
-        saveUpdateAction(proceedingJoinPoint, groupId, operation);
+        saveUpdateAction(NiFiUserUtils.getNiFiUser(), groupId, operation);
+
+        return result;
     }
 
 
@@ -193,51 +202,117 @@ public class ProcessGroupAuditor extends NiFiAuditor {
      *
      * @param proceedingJoinPoint join point
      * @param groupId group id
-     * @param state controller serivce state state
+     * @param state controller service state
      * @throws Throwable ex
      */
     @Around("within(org.apache.nifi.web.dao.ProcessGroupDAO+) && "
-        + "execution(java.util.concurrent.Future activateControllerServices(java.lang.String, org.apache.nifi.controller.service.ControllerServiceState, java.util.Set)) && "
+        + "execution(java.util.concurrent.Future<Void> activateControllerServices(java.lang.String, org.apache.nifi.controller.service.ControllerServiceState, java.util.Set)) && "
         + "args(groupId, state)")
-    public void activateControllerServicesAdvice(ProceedingJoinPoint proceedingJoinPoint, String groupId, ControllerServiceState state) throws Throwable {
+    public Future<Void> activateControllerServicesAdvice(ProceedingJoinPoint proceedingJoinPoint, String groupId, ControllerServiceState state) throws Throwable {
+        final Operation operation;
+
+        final Future<Void> result = (Future<Void>) proceedingJoinPoint.proceed();
 
         // determine the service state
-        final Operation operation;
         if (ControllerServiceState.ENABLED.equals(state)) {
             operation = Operation.Enable;
         } else {
             operation = Operation.Disable;
         }
 
-        saveUpdateAction(proceedingJoinPoint, groupId, operation);
+        saveUpdateAction(NiFiUserUtils.getNiFiUser(), groupId, operation);
+
+        return result;
     }
 
     /**
      * Audits the update of process group variable registry.
      *
      * @param proceedingJoinPoint join point
-     * @param groupId group id
+     * @param user the user performing the action
+     * @param variableRegistry variable registry
      * @throws Throwable ex
      */
     @Around("within(org.apache.nifi.web.dao.ProcessGroupDAO+) && "
-        + "execution(org.apache.nifi.groups.ProcessGroup updateVariableRegistry(org.apache.nifi.web.api.dto.VariableRegistryDTO)) && "
-        + "args(groupId)")
-    public void updateVariableRegistryAdvice(ProceedingJoinPoint proceedingJoinPoint, String groupId) throws Throwable {
-        final Operation operation = Operation.Configure;
-        saveUpdateAction(proceedingJoinPoint, groupId, operation);
+        + "execution(org.apache.nifi.groups.ProcessGroup updateVariableRegistry(org.apache.nifi.authorization.user.NiFiUser, org.apache.nifi.web.api.dto.VariableRegistryDTO)) && "
+        + "args(user, variableRegistry)")
+    public ProcessGroup updateVariableRegistryAdvice(final ProceedingJoinPoint proceedingJoinPoint, final NiFiUser user, final VariableRegistryDTO variableRegistry) throws Throwable {
+        final ProcessGroup updatedProcessGroup = (ProcessGroup) proceedingJoinPoint.proceed();
+
+        saveUpdateAction(user, variableRegistry.getProcessGroupId(), Operation.Configure);
+
+        return updatedProcessGroup;
     }
 
+    @Around("within(org.apache.nifi.web.dao.ProcessGroupDAO+) && "
+            + "execution(org.apache.nifi.groups.ProcessGroup updateProcessGroupFlow(..))")
+    public ProcessGroup updateProcessGroupFlowAdvice(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
+        final Object[] args = proceedingJoinPoint.getArgs();
+        final String groupId = (String) args[0];
+        final NiFiUser user = (NiFiUser) args[1];
 
+        final ProcessGroupDAO processGroupDAO = getProcessGroupDAO();
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final VersionControlInformation vci = processGroup.getVersionControlInformation();
 
-    private void saveUpdateAction(final ProceedingJoinPoint proceedingJoinPoint, final String groupId, final Operation operation) throws Throwable {
-        ProcessGroupDAO processGroupDAO = getProcessGroupDAO();
-        ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final ProcessGroup updatedProcessGroup = (ProcessGroup) proceedingJoinPoint.proceed();
+        final VersionControlInformation updatedVci = updatedProcessGroup.getVersionControlInformation();
 
-        // perform the action
-        proceedingJoinPoint.proceed();
+        final Operation operation;
+        if (vci == null) {
+            operation = Operation.StartVersionControl;
+        } else {
+            if (updatedVci == null) {
+                operation = Operation.StopVersionControl;
+            } else if (vci.getVersion() == updatedVci.getVersion()) {
+                operation = Operation.RevertLocalChanges;
+            } else {
+                operation = Operation.ChangeVersion;
+            }
+        }
 
-        // get the current user
-        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        saveUpdateAction(user, groupId, operation);
+
+        return updatedProcessGroup;
+    }
+
+    @Around("within(org.apache.nifi.web.dao.ProcessGroupDAO+) && "
+            + "execution(org.apache.nifi.groups.ProcessGroup updateVersionControlInformation(..))")
+    public ProcessGroup updateVersionControlInformationAdvice(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
+        final VersionControlInformationDTO vciDto = (VersionControlInformationDTO) proceedingJoinPoint.getArgs()[0];
+
+        final ProcessGroupDAO processGroupDAO = getProcessGroupDAO();
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(vciDto.getGroupId());
+        final VersionControlInformation vci = processGroup.getVersionControlInformation();
+
+        final ProcessGroup updatedProcessGroup = (ProcessGroup) proceedingJoinPoint.proceed();
+
+        final Operation operation;
+        if (vci == null) {
+            operation = Operation.StartVersionControl;
+        } else {
+            operation = Operation.CommitLocalChanges;
+        }
+
+        saveUpdateAction(NiFiUserUtils.getNiFiUser(), vciDto.getGroupId(), operation);
+
+        return updatedProcessGroup;
+    }
+
+    @Around("within(org.apache.nifi.web.dao.ProcessGroupDAO+) && "
+            + "execution(org.apache.nifi.groups.ProcessGroup disconnectVersionControl(java.lang.String)) && "
+            + "args(groupId)")
+    public ProcessGroup disconnectVersionControlAdvice(final ProceedingJoinPoint proceedingJoinPoint, final String groupId) throws Throwable {
+        final ProcessGroup updatedProcessGroup = (ProcessGroup) proceedingJoinPoint.proceed();
+
+        saveUpdateAction(NiFiUserUtils.getNiFiUser(), groupId, Operation.StopVersionControl);
+
+        return updatedProcessGroup;
+    }
+
+    private void saveUpdateAction(final NiFiUser user, final String groupId, final Operation operation) throws Throwable {
+        ProcessGroupDAO processGroupDAO = getProcessGroupDAO();
+        ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
 
         // if the user was starting/stopping this process group
         FlowChangeAction action = new FlowChangeAction();

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index be77d10..78335f4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -16,14 +16,6 @@
  */
 package org.apache.nifi.web;
 
-import java.io.IOException;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-
 import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
@@ -106,6 +98,7 @@ import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
+import org.apache.nifi.web.api.entity.RegistryClientEntity;
 import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupPortEntity;
@@ -124,6 +117,14 @@ import org.apache.nifi.web.api.entity.VersionControlInformationEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowEntity;
 import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataEntity;
 
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
 /**
  * Defines the NiFiServiceFacade interface.
  */
@@ -1929,7 +1930,7 @@ public interface NiFiServiceFacade {
      * @param registryDTO The registry DTO
      * @return The reporting task DTO
      */
-    RegistryEntity createRegistryClient(Revision revision, RegistryDTO registryDTO);
+    RegistryClientEntity createRegistryClient(Revision revision, RegistryDTO registryDTO);
 
     /**
      * Gets a registry with the specified id.
@@ -1937,14 +1938,14 @@ public interface NiFiServiceFacade {
      * @param registryId id
      * @return entity
      */
-    RegistryEntity getRegistryClient(String registryId);
+    RegistryClientEntity getRegistryClient(String registryId);
 
     /**
      * Returns all registry clients.
      *
      * @return registry clients
      */
-    Set<RegistryEntity> getRegistryClients();
+    Set<RegistryClientEntity> getRegistryClients();
 
     /**
      * Gets all registries for the current user.
@@ -1991,7 +1992,7 @@ public interface NiFiServiceFacade {
      * @param registryDTO the registry dto
      * @return the updated registry registry entity
      */
-    RegistryEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO);
+    RegistryClientEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO);
 
     /**
      * Deletes the specified registry using the specified revision.
@@ -2000,7 +2001,7 @@ public interface NiFiServiceFacade {
      * @param registryId id
      * @return the deleted registry entity
      */
-    RegistryEntity deleteRegistryClient(Revision revision, String registryId);
+    RegistryClientEntity deleteRegistryClient(Revision revision, String registryId);
 
     /**
      * Verifies the specified registry can be removed.

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 4adb85b..ae89ef0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -220,6 +220,7 @@ import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
+import org.apache.nifi.web.api.entity.RegistryClientEntity;
 import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupPortEntity;
@@ -927,7 +928,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId());
         final RevisionUpdate<VariableRegistryDTO> snapshot = updateComponent(user, revision,
             processGroupNode,
-            () -> processGroupDAO.updateVariableRegistry(variableRegistryDto),
+            () -> processGroupDAO.updateVariableRegistry(user, variableRegistryDto),
             processGroup -> dtoFactory.createVariableRegistryDto(processGroup, revisionManager));
 
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);
@@ -2301,10 +2302,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
 
 
     @Override
-    public RegistryEntity createRegistryClient(Revision revision, RegistryDTO registryDTO) {
+    public RegistryClientEntity createRegistryClient(Revision revision, RegistryDTO registryDTO) {
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
-        // read lock on the containing group
         // request claim for component to be created... revision already verified (version == 0)
         final RevisionClaim claim = new StandardRevisionClaim(revision);
 
@@ -2321,52 +2321,25 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         });
 
         final FlowRegistry registry = revisionUpdate.getComponent();
-        return createRegistryEntity(registry);
+        return createRegistryClientEntity(registry);
     }
 
     @Override
-    public RegistryEntity getRegistryClient(final String registryId) {
+    public RegistryClientEntity getRegistryClient(final String registryId) {
         final FlowRegistry registry = registryDAO.getFlowRegistry(registryId);
-        return createRegistryEntity(registry);
+        return createRegistryClientEntity(registry);
     }
 
-    private RegistryEntity createRegistryEntity(final FlowRegistry flowRegistry) {
+    private RegistryClientEntity createRegistryClientEntity(final FlowRegistry flowRegistry) {
         if (flowRegistry == null) {
             return null;
         }
 
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(flowRegistry.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getController());
         final RegistryDTO dto = dtoFactory.createRegistryDto(flowRegistry);
-        final Revision revision = revisionManager.getRevision(dto.getId());
-
-        final RegistryEntity entity = new RegistryEntity();
-        entity.setComponent(dto);
-        entity.setRevision(dtoFactory.createRevisionDTO(revision));
-        entity.setId(dto.getId());
 
-        // User who created it can read/write it.
-        final PermissionsDTO permissions = new PermissionsDTO();
-        permissions.setCanRead(true);
-        permissions.setCanWrite(true);
-        entity.setPermissions(permissions);
-
-        return entity;
-    }
-
-    private BucketEntity createBucketEntity(final Bucket bucket) {
-        if (bucket == null) {
-            return null;
-        }
-
-        final BucketDTO dto = new BucketDTO();
-        dto.setId(bucket.getIdentifier());
-        dto.setName(bucket.getName());
-        dto.setDescription(bucket.getDescription());
-        dto.setCreated(bucket.getCreatedTimestamp());
-
-        final BucketEntity entity = new BucketEntity();
-        entity.setBucket(dto);
-
-        return entity;
+        return entityFactory.createRegistryClientEntity(dto, revision, permissions);
     }
 
     private VersionedFlowEntity createVersionedFlowEntity(final String registryId, final VersionedFlow versionedFlow) {
@@ -2400,23 +2373,40 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public Set<RegistryEntity> getRegistryClients() {
+    public Set<RegistryClientEntity> getRegistryClients() {
         return registryDAO.getFlowRegistries().stream()
-            .map(this::createRegistryEntity)
+            .map(this::createRegistryClientEntity)
             .collect(Collectors.toSet());
     }
 
     @Override
     public Set<RegistryEntity> getRegistriesForUser(final NiFiUser user) {
         return registryDAO.getFlowRegistriesForUser(user).stream()
-                .map(this::createRegistryEntity)
+                .map(flowRegistry -> entityFactory.createRegistryEntity(dtoFactory.createRegistryDto(flowRegistry)))
                 .collect(Collectors.toSet());
     }
 
     @Override
     public Set<BucketEntity> getBucketsForUser(final String registryId, final NiFiUser user) {
         return registryDAO.getBucketsForUser(registryId, user).stream()
-                .map(this::createBucketEntity)
+                .map(bucket -> {
+                    if (bucket == null) {
+                        return null;
+                    }
+
+                    final BucketDTO dto = new BucketDTO();
+                    dto.setId(bucket.getIdentifier());
+                    dto.setName(bucket.getName());
+                    dto.setDescription(bucket.getDescription());
+                    dto.setCreated(bucket.getCreatedTimestamp());
+
+                    final Set<String> authorizedActions = bucket.getAuthorizedActions();
+                    final PermissionsDTO permissions = new PermissionsDTO();
+                    permissions.setCanRead(authorizedActions.contains("read"));
+                    permissions.setCanWrite(authorizedActions.contains("write"));
+
+                    return entityFactory.createBucketEntity(dto, permissions);
+                })
                 .collect(Collectors.toSet());
     }
 
@@ -2435,7 +2425,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public RegistryEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO) {
+    public RegistryClientEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO) {
         final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
@@ -2454,7 +2444,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         });
 
         final FlowRegistry updatedReg = revisionUpdate.getComponent();
-        return createRegistryEntity(updatedReg);
+        return createRegistryClientEntity(updatedReg);
     }
 
     @Override
@@ -2463,7 +2453,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public RegistryEntity deleteRegistryClient(final Revision revision, final String registryId) {
+    public RegistryClientEntity deleteRegistryClient(final Revision revision, final String registryId) {
         final RevisionClaim claim = new StandardRevisionClaim(revision);
         final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
@@ -2473,7 +2463,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             return reg;
         });
 
-        return createRegistryEntity(registry);
+        return createRegistryClientEntity(registry);
     }
 
     @Override
@@ -3695,10 +3685,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             // add first snapshot to the flow in the registry
             registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, versionedFlowDto.getComments(), expectedVersion);
         } catch (final NiFiRegistryException e) {
-            throw new IllegalArgumentException(e);
+            throw new IllegalArgumentException(e.getLocalizedMessage());
         } catch (final IOException ioe) {
-            // will result in a 500: Internal Server Error
-            throw new RuntimeException("Failed to communicate with Flow Registry when attempting to " + action);
+            throw new IllegalStateException("Failed to communicate with Flow Registry when attempting to " + action);
         }
 
         final Bucket bucket = registeredSnapshot.getBucket();
@@ -4105,7 +4094,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(groupId);
         final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(user, revision,
             processGroupNode,
-            () -> processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows),
+            () -> processGroupDAO.updateProcessGroupFlow(groupId, user, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows),
             processGroup -> dtoFactory.createProcessGroupDto(processGroup));
 
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
index 356d231..04c0efa 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
@@ -46,8 +46,8 @@ import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.Entity;
 import org.apache.nifi.web.api.entity.HistoryEntity;
 import org.apache.nifi.web.api.entity.NodeEntity;
-import org.apache.nifi.web.api.entity.RegistriesEntity;
-import org.apache.nifi.web.api.entity.RegistryEntity;
+import org.apache.nifi.web.api.entity.RegistryClientsEntity;
+import org.apache.nifi.web.api.entity.RegistryClientEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
 import org.apache.nifi.web.api.request.DateTimeParameter;
@@ -91,12 +91,12 @@ public class ControllerResource extends ApplicationResource {
     /**
      * Populate the uri's for the specified registry.
      *
-     * @param registryEntity registry
+     * @param registryClientEntity registry
      * @return dtos
      */
-    public RegistryEntity populateRemainingRegistryEntityContent(final RegistryEntity registryEntity) {
-        registryEntity.setUri(generateResourceUri("controller", "registry-clients", registryEntity.getId()));
-        return registryEntity;
+    public RegistryClientEntity populateRemainingRegistryEntityContent(final RegistryClientEntity registryClientEntity) {
+        registryClientEntity.setUri(generateResourceUri("controller", "registry-clients", registryClientEntity.getId()));
+        return registryClientEntity;
     }
 
     /**
@@ -316,7 +316,7 @@ public class ControllerResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("registry-clients")
-    @ApiOperation(value = "Gets the listing of available registry clients", response = RegistriesEntity.class, authorizations = {
+    @ApiOperation(value = "Gets the listing of available registry clients", response = RegistryClientsEntity.class, authorizations = {
             @Authorization(value = "Read - /flow")
     })
     @ApiResponses(value = {
@@ -333,10 +333,10 @@ public class ControllerResource extends ApplicationResource {
             return replicate(HttpMethod.GET);
         }
 
-        final Set<RegistryEntity> registries = serviceFacade.getRegistryClients();
+        final Set<RegistryClientEntity> registries = serviceFacade.getRegistryClients();
         registries.forEach(registry -> populateRemainingRegistryEntityContent(registry));
 
-        final RegistriesEntity registryEntities = new RegistriesEntity();
+        final RegistryClientsEntity registryEntities = new RegistryClientsEntity();
         registryEntities.setRegistries(registries);
 
         return generateOkResponse(registryEntities).build();
@@ -346,8 +346,8 @@ public class ControllerResource extends ApplicationResource {
      * Creates a new Registry.
      *
      * @param httpServletRequest  request
-     * @param requestRegistryEntity A registryEntity.
-     * @return A registryEntity.
+     * @param requestRegistryClientEntity A registryClientEntity.
+     * @return A registryClientEntity.
      */
     @POST
     @Consumes(MediaType.APPLICATION_JSON)
@@ -355,7 +355,7 @@ public class ControllerResource extends ApplicationResource {
     @Path("registry-clients")
     @ApiOperation(
             value = "Creates a new registry client",
-            response = RegistryEntity.class,
+            response = RegistryClientEntity.class,
             authorizations = {
                     @Authorization(value = "Write - /controller")
             }
@@ -373,28 +373,28 @@ public class ControllerResource extends ApplicationResource {
             @ApiParam(
                     value = "The registry configuration details.",
                     required = true
-            ) final RegistryEntity requestRegistryEntity) {
+            ) final RegistryClientEntity requestRegistryClientEntity) {
 
-        if (requestRegistryEntity == null || requestRegistryEntity.getComponent() == null) {
+        if (requestRegistryClientEntity == null || requestRegistryClientEntity.getComponent() == null) {
             throw new IllegalArgumentException("Registry details must be specified.");
         }
 
-        if (requestRegistryEntity.getRevision() == null || (requestRegistryEntity.getRevision().getVersion() == null || requestRegistryEntity.getRevision().getVersion() != 0)) {
+        if (requestRegistryClientEntity.getRevision() == null || (requestRegistryClientEntity.getRevision().getVersion() == null || requestRegistryClientEntity.getRevision().getVersion() != 0)) {
             throw new IllegalArgumentException("A revision of 0 must be specified when creating a new Registry.");
         }
 
-        final RegistryDTO requestReportingTask = requestRegistryEntity.getComponent();
+        final RegistryDTO requestReportingTask = requestRegistryClientEntity.getComponent();
         if (requestReportingTask.getId() != null) {
             throw new IllegalArgumentException("Registry ID cannot be specified.");
         }
 
         if (isReplicateRequest()) {
-            return replicate(HttpMethod.POST, requestRegistryEntity);
+            return replicate(HttpMethod.POST, requestRegistryClientEntity);
         }
 
         return withWriteLock(
                 serviceFacade,
-                requestRegistryEntity,
+                requestRegistryClientEntity,
                 lookup -> {
                     authorizeController(RequestAction.WRITE);
                 },
@@ -407,7 +407,7 @@ public class ControllerResource extends ApplicationResource {
 
                     // create the reporting task and generate the json
                     final Revision revision = getRevision(registryEntity, registry.getId());
-                    final RegistryEntity entity = serviceFacade.createRegistryClient(revision, registry);
+                    final RegistryClientEntity entity = serviceFacade.createRegistryClient(revision, registry);
                     populateRemainingRegistryEntityContent(entity);
 
                     // build the response
@@ -420,7 +420,7 @@ public class ControllerResource extends ApplicationResource {
      * Retrieves the specified registry.
      *
      * @param id The id of the registry to retrieve
-     * @return A registryEntity.
+     * @return A registryClientEntity.
      */
     @GET
     @Consumes(MediaType.WILDCARD)
@@ -428,7 +428,7 @@ public class ControllerResource extends ApplicationResource {
     @Path("/registry-clients/{id}")
     @ApiOperation(
             value = "Gets a registry client",
-            response = RegistryEntity.class,
+            response = RegistryClientEntity.class,
             authorizations = {
                     @Authorization(value = "Read - /controller")
             }
@@ -442,7 +442,7 @@ public class ControllerResource extends ApplicationResource {
                     @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
             }
     )
-    public Response getRegistry(
+    public Response getRegistryClient(
             @ApiParam(
                     value = "The registry id.",
                     required = true
@@ -457,7 +457,7 @@ public class ControllerResource extends ApplicationResource {
         authorizeController(RequestAction.READ);
 
         // get the registry
-        final RegistryEntity entity = serviceFacade.getRegistryClient(id);
+        final RegistryClientEntity entity = serviceFacade.getRegistryClient(id);
         populateRemainingRegistryEntityContent(entity);
 
         return generateOkResponse(entity).build();
@@ -477,7 +477,7 @@ public class ControllerResource extends ApplicationResource {
     @Path("/registry-clients/{id}")
     @ApiOperation(
             value = "Updates a registry client",
-            response = RegistryEntity.class,
+            response = RegistryClientEntity.class,
             authorizations = {
                     @Authorization(value = "Write - /controller")
             }
@@ -501,7 +501,7 @@ public class ControllerResource extends ApplicationResource {
             @ApiParam(
                     value = "The registry configuration details.",
                     required = true
-            ) final RegistryEntity requestRegsitryEntity) {
+            ) final RegistryClientEntity requestRegsitryEntity) {
 
         if (requestRegsitryEntity == null || requestRegsitryEntity.getComponent() == null) {
             throw new IllegalArgumentException("Registry details must be specified.");
@@ -536,7 +536,7 @@ public class ControllerResource extends ApplicationResource {
                     final RegistryDTO registry = registryEntity.getComponent();
 
                     // update the controller service
-                    final RegistryEntity entity = serviceFacade.updateRegistryClient(revision, registry);
+                    final RegistryClientEntity entity = serviceFacade.updateRegistryClient(revision, registry);
                     populateRemainingRegistryEntityContent(entity);
 
                     return generateOkResponse(entity).build();
@@ -562,7 +562,7 @@ public class ControllerResource extends ApplicationResource {
     @Path("/registry-clients/{id}")
     @ApiOperation(
             value = "Deletes a registry client",
-            response = RegistryEntity.class,
+            response = RegistryClientEntity.class,
             authorizations = {
                     @Authorization(value = "Write - /controller")
             }
@@ -598,14 +598,14 @@ public class ControllerResource extends ApplicationResource {
             return replicate(HttpMethod.DELETE);
         }
 
-        final RegistryEntity requestRegistryEntity = new RegistryEntity();
-        requestRegistryEntity.setId(id);
+        final RegistryClientEntity requestRegistryClientEntity = new RegistryClientEntity();
+        requestRegistryClientEntity.setId(id);
 
         // handle expects request (usually from the cluster manager)
         final Revision requestRevision = new Revision(version == null ? null : version.getLong(), clientId.getClientId(), id);
         return withWriteLock(
                 serviceFacade,
-                requestRegistryEntity,
+                requestRegistryClientEntity,
                 requestRevision,
                 lookup -> {
                     authorizeController(RequestAction.WRITE);
@@ -613,7 +613,7 @@ public class ControllerResource extends ApplicationResource {
                 () -> serviceFacade.verifyDeleteRegistry(id),
                 (revision, registryEntity) -> {
                     // delete the specified registry
-                    final RegistryEntity entity = serviceFacade.deleteRegistryClient(revision, registryEntity.getId());
+                    final RegistryClientEntity entity = serviceFacade.deleteRegistryClient(revision, registryEntity.getId());
                     return generateOkResponse(entity).build();
                 }
         );

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index 0ae5864..dad8039 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -88,6 +88,7 @@ import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorTypesEntity;
 import org.apache.nifi.web.api.entity.RegistriesEntity;
+import org.apache.nifi.web.api.entity.RegistryClientsEntity;
 import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
 import org.apache.nifi.web.api.entity.ReportingTaskEntity;
@@ -1316,7 +1317,7 @@ public class FlowResource extends ApplicationResource {
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("registries")
-    @ApiOperation(value = "Gets the listing of available registries", response = RegistriesEntity.class, authorizations = {
+    @ApiOperation(value = "Gets the listing of available registries", response = RegistryClientsEntity.class, authorizations = {
             @Authorization(value = "Read - /flow")
     })
     @ApiResponses(value = {

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 6dd641b..5dc7325 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -183,7 +183,7 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
     public Response createVersionControlRequest(
-            @ApiParam(value = "The versioned flow details.", required = true) final CreateActiveRequestEntity requestEntity) throws InterruptedException {
+            @ApiParam(value = "The versioned flow details.", required = true) final CreateActiveRequestEntity requestEntity) {
 
         if (isReplicateRequest()) {
             return replicate(HttpMethod.POST);
@@ -412,7 +412,7 @@ public class VersionsResource extends ApplicationResource {
     })
     public Response saveToFlowRegistry(
         @ApiParam("The process group id.") @PathParam("id") final String groupId,
-        @ApiParam(value = "The versioned flow details.", required = true) final StartVersionControlRequestEntity requestEntity) throws IOException {
+        @ApiParam(value = "The versioned flow details.", required = true) final StartVersionControlRequestEntity requestEntity) {
 
         // Verify the request
         final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();
@@ -569,8 +569,9 @@ public class VersionsResource extends ApplicationResource {
         return requestId;
     }
 
-    private void replicateVersionControlMapping(final VersionControlComponentMappingEntity mappingEntity, final StartVersionControlRequestEntity requestEntity, final URI requestUri,
-        final String groupId) {
+    private void replicateVersionControlMapping(final VersionControlComponentMappingEntity mappingEntity, final StartVersionControlRequestEntity requestEntity,
+                                                final URI requestUri, final String groupId) {
+
         final Map<String, String> headers = new HashMap<>();
         headers.put("content-type", MediaType.APPLICATION_JSON);
 
@@ -652,7 +653,7 @@ public class VersionsResource extends ApplicationResource {
                 value = "If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.",
                 required = false)
         @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) final ClientIdParameter clientId,
-        @ApiParam("The process group id.") @PathParam("id") final String groupId) throws IOException {
+        @ApiParam("The process group id.") @PathParam("id") final String groupId) {
 
         if (isReplicateRequest()) {
             return replicate(HttpMethod.DELETE);
@@ -705,7 +706,7 @@ public class VersionsResource extends ApplicationResource {
         @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
     })
     public Response updateFlowVersion(@ApiParam("The process group id.") @PathParam("id") final String groupId,
-        @ApiParam(value = "The controller service configuration details.", required = true) final VersionedFlowSnapshotEntity requestEntity) throws IOException, LifecycleManagementException {
+        @ApiParam(value = "The controller service configuration details.", required = true) final VersionedFlowSnapshotEntity requestEntity) {
 
         // Verify the request
         final RevisionDTO revisionDto = requestEntity.getProcessGroupRevision();

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 7a1442d..ca781a4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -272,10 +272,10 @@ public final class DtoFactory {
         actionDto.setId(action.getId());
         actionDto.setSourceId(action.getSourceId());
         actionDto.setSourceName(action.getSourceName());
-        actionDto.setSourceType(action.getSourceType().name());
+        actionDto.setSourceType(action.getSourceType().toString());
         actionDto.setTimestamp(action.getTimestamp());
         actionDto.setUserIdentity(action.getUserIdentity());
-        actionDto.setOperation(action.getOperation().name());
+        actionDto.setOperation(action.getOperation().toString());
         actionDto.setActionDetails(createActionDetailsDto(action.getActionDetails()));
         actionDto.setComponentDetails(createComponentDetailsDto(action.getComponentDetails()));
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index 1e6167c..34f4997 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -35,6 +35,7 @@ import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.AllowableValueEntity;
+import org.apache.nifi.web.api.entity.BucketEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
@@ -56,6 +57,8 @@ import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusEntity;
 import org.apache.nifi.web.api.entity.ProcessorStatusSnapshotEntity;
+import org.apache.nifi.web.api.entity.RegistryClientEntity;
+import org.apache.nifi.web.api.entity.RegistryEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupPortEntity;
 import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusEntity;
@@ -546,4 +549,43 @@ public final class EntityFactory {
         entity.setProcessGroupRevision(processGroupRevision);
         return entity;
     }
+
+    public RegistryClientEntity createRegistryClientEntity(final RegistryDTO dto, final RevisionDTO revision, final PermissionsDTO permissions) {
+        final RegistryClientEntity entity = new RegistryClientEntity();
+        entity.setRevision(revision);
+        entity.setPermissions(permissions);
+
+        if (dto != null) {
+            entity.setId(dto.getId());
+
+            if (permissions != null && permissions.getCanRead()) {
+                entity.setComponent(dto);
+            }
+        }
+
+        return entity;
+    }
+
+    public RegistryEntity createRegistryEntity(final RegistryDTO dto) {
+        final RegistryEntity entity = new RegistryEntity();
+
+        if (dto != null) {
+            entity.setRegistry(dto);
+        }
+
+        return entity;
+    }
+
+    public BucketEntity createBucketEntity(final BucketDTO dto, final PermissionsDTO permissions) {
+        final BucketEntity entity = new BucketEntity();
+        entity.setId(dto.getId());
+        entity.setPermissions(permissions);
+
+        if (permissions != null && permissions.getCanRead()) {
+            entity.setBucket(dto);
+        }
+
+        return entity;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
index 9259bf4..459acfc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessGroupDAO.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.web.dao;
 
+import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
 import org.apache.nifi.groups.ProcessGroup;
@@ -118,8 +119,8 @@ public interface ProcessGroupDAO {
      *            update the contents of that Process Group
      * @return the process group
      */
-    ProcessGroup updateProcessGroupFlow(String groupId, VersionedFlowSnapshot proposedSnapshot, VersionControlInformationDTO versionControlInformation, String componentIdSeed,
-        boolean verifyNotModified, boolean updateSettings, boolean updateDescendantVersionedFlows);
+    ProcessGroup updateProcessGroupFlow(String groupId, NiFiUser user, VersionedFlowSnapshot proposedSnapshot, VersionControlInformationDTO versionControlInformation, String componentIdSeed,
+                                        boolean verifyNotModified, boolean updateSettings, boolean updateDescendantVersionedFlows);
 
     /**
      * Applies the given Version Control Information to the Process Group
@@ -141,10 +142,11 @@ public interface ProcessGroupDAO {
     /**
      * Updates the specified variable registry
      *
+     * @param user the user performing the update
      * @param variableRegistry the Variable Registry
      * @return the Process Group that was updated
      */
-    ProcessGroup updateVariableRegistry(VariableRegistryDTO variableRegistry);
+    ProcessGroup updateVariableRegistry(NiFiUser user, VariableRegistryDTO variableRegistry);
 
     /**
      * Verifies that the specified updates to a current Process Group can be applied at this time

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
index 4f5af74..7d3b567 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/FlowRegistryDAO.java
@@ -17,11 +17,6 @@
 
 package org.apache.nifi.web.dao.impl;
 
-import java.io.IOException;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.client.NiFiRegistryException;
@@ -36,6 +31,7 @@ import org.apache.nifi.web.dao.RegistryDAO;
 
 import java.io.IOException;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.stream.Collectors;
 
 public class FlowRegistryDAO implements RegistryDAO {
@@ -65,7 +61,6 @@ public class FlowRegistryDAO implements RegistryDAO {
 
     @Override
     public Set<FlowRegistry> getFlowRegistriesForUser(final NiFiUser user) {
-        // TODO - implement to be user specific
         return getFlowRegistries();
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
index 1aaf4cc..d25f294 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessGroupDAO.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.web.dao.impl;
 
+import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.connectable.Position;
@@ -143,7 +144,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
-    public CompletableFuture<Void> scheduleComponents(final String groupId, final ScheduledState state, final Set<String> componentIds) {
+    public Future<Void> scheduleComponents(final String groupId, final ScheduledState state, final Set<String> componentIds) {
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
 
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
@@ -275,8 +276,9 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
-    public ProcessGroup updateProcessGroupFlow(final String groupId, final VersionedFlowSnapshot proposedSnapshot, final VersionControlInformationDTO versionControlInformation,
-        final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) {
+    public ProcessGroup updateProcessGroupFlow(final String groupId, final NiFiUser user, final VersionedFlowSnapshot proposedSnapshot, final VersionControlInformationDTO versionControlInformation,
+                                               final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) {
+
         final ProcessGroup group = locateProcessGroup(flowController, groupId);
         group.updateFlow(proposedSnapshot, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows);
 
@@ -291,7 +293,7 @@ public class StandardProcessGroupDAO extends ComponentDAO implements ProcessGrou
     }
 
     @Override
-    public ProcessGroup updateVariableRegistry(final VariableRegistryDTO variableRegistry) {
+    public ProcessGroup updateVariableRegistry(final NiFiUser user, final VariableRegistryDTO variableRegistry) {
         final ProcessGroup group = locateProcessGroup(flowController, variableRegistry.getProcessGroupId());
         if (group == null) {
             throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistry.getProcessGroupId());

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
index f26c2eb..4bbd9c8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
@@ -80,6 +80,7 @@
             </div>
         </div>
     </div>
+    <div id="variable-message">Variables do not support sensitive values and will be included when versioning a Process Group.</div>
 </div>
 <div id="new-variable-dialog" class="dialog cancellable small-dialog hidden">
     <div class="dialog-content">

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/ok-dialog.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/ok-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/ok-dialog.jsp
index 92a3d4a..826a1ff 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/ok-dialog.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/ok-dialog.jsp
@@ -15,7 +15,7 @@
   limitations under the License.
 --%>
 <%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
-<div id="nf-ok-dialog" class="hidden small-dialog">
+<div id="nf-ok-dialog" class="hidden medium-short-dialog">
     <div class="dialog-content">
         <div id="nf-ok-dialog-content"></div>
     </div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index 9acbdde..710c0fb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -323,6 +323,13 @@ div.slick-cell div.overridden {
     text-decoration: line-through;
 }
 
+#variable-message {
+    position: absolute;
+    top: 550px;
+    left: 20px;
+    font-size: 13px;
+}
+
 /*
     Registry configuration dialog
  */

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/modal/jquery.modal.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/modal/jquery.modal.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/modal/jquery.modal.css
index d998436..32aa8f3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/modal/jquery.modal.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/modal/jquery.modal.css
@@ -43,6 +43,15 @@
     min-width: 470px;
 }
 
+.medium-short-dialog {
+    max-height: 32%;
+    max-width: 34%;
+    min-height: 250px;
+    min-width: 440px;
+    font-size: 13px;
+    line-height: 1.3;
+}
+
 .large-dialog {
     max-height: 72%;
     max-width: 55%;

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
index 725b6a1..8a17291 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
@@ -920,7 +920,7 @@
         'delete': function (selection) {
             if (nfCommon.isUndefined(selection) || selection.empty()) {
                 nfDialog.showOkDialog({
-                    headerText: 'Reload',
+                    headerText: 'Delete Components',
                     dialogContent: 'No eligible components are selected. Please select the components to be deleted.'
                 });
             } else {

http://git-wip-us.apache.org/repos/asf/nifi/blob/db2cc9fe/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
index 9ef210e..f1b9a45 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -180,9 +180,11 @@
      * @param registryCombo
      * @param bucketCombo
      * @param flowCombo
+     * @param selectBucket
+     * @param bucketCheck
      * @returns {deferred}
      */
-    var loadRegistries = function (dialog, registryCombo, bucketCombo, flowCombo, selectBucket) {
+    var loadRegistries = function (dialog, registryCombo, bucketCombo, flowCombo, selectBucket, bucketCheck) {
         return $.ajax({
             type: 'GET',
             url: '../nifi-api/flow/registries',
@@ -192,11 +194,11 @@
 
             if (nfCommon.isDefinedAndNotNull(registriesResponse.registries) && registriesResponse.registries.length > 0) {
                 registriesResponse.registries.sort(function (a, b) {
-                    return a.component.name > b.component.name;
+                    return a.registry.name > b.registry.name;
                 });
 
                 $.each(registriesResponse.registries, function (_, registryEntity) {
-                    var registry = registryEntity.component;
+                    var registry = registryEntity.registry;
                     registries.push({
                         text: registry.name,
                         value: registry.id,
@@ -216,7 +218,7 @@
             registryCombo.combo({
                 options: registries,
                 select: function (selectedOption) {
-                    selectRegistry(dialog, selectedOption, bucketCombo, flowCombo, selectBucket)
+                    selectRegistry(dialog, selectedOption, bucketCombo, flowCombo, selectBucket, bucketCheck)
                 }
             });
         }).fail(nfErrorHandler.handleAjaxError);
@@ -229,9 +231,10 @@
      * @param bucketCombo
      * @param flowCombo
      * @param selectBucket
+     * @param bucketCheck
      * @returns {*}
      */
-    var loadBuckets = function (registryIdentifier, bucketCombo, flowCombo, selectBucket) {
+    var loadBuckets = function (registryIdentifier, bucketCombo, flowCombo, selectBucket, bucketCheck) {
         return $.ajax({
             type: 'GET',
             url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets',
@@ -241,18 +244,33 @@
 
             if (nfCommon.isDefinedAndNotNull(response.buckets) && response.buckets.length > 0) {
                 response.buckets.sort(function (a, b) {
+                    if (a.permissions.canRead === false && b.permissions.canRead === false) {
+                        return 0;
+                    } else if (a.permissions.canRead === false) {
+                        return -1;
+                    } else if (b.permissions.canRead === false) {
+                        return 1;
+                    }
+
                     return a.bucket.name > b.bucket.name;
                 });
 
                 $.each(response.buckets, function (_, bucketEntity) {
-                    var bucket = bucketEntity.bucket;
-                    buckets.push({
-                        text: bucket.name,
-                        value: bucket.id,
-                        description: nfCommon.escapeHtml(bucket.description)
-                    });
+                    if (bucketEntity.permissions.canRead === true) {
+                        var bucket = bucketEntity.bucket;
+
+                        if (bucketCheck(bucketEntity)) {
+                            buckets.push({
+                                text: bucket.name,
+                                value: bucket.id,
+                                description: nfCommon.escapeHtml(bucket.description)
+                            });
+                        }
+                    }
                 });
-            } else {
+            }
+
+            if (buckets.length === 0) {
                 buckets.push({
                     text: 'No available buckets',
                     value: null,
@@ -285,8 +303,9 @@
      * @param bucketCombo
      * @param flowCombo
      * @param selectBucket
+     * @param bucketCheck
      */
-    var selectRegistry = function (dialog, selectedOption, bucketCombo, flowCombo, selectBucket) {
+    var selectRegistry = function (dialog, selectedOption, bucketCombo, flowCombo, selectBucket, bucketCheck) {
         var showNoBucketsAvailable = function () {
             bucketCombo.combo('destroy').combo({
                 options: [{
@@ -333,7 +352,7 @@
                 clearFlowVersionsGrid();
             }
 
-            loadBuckets(selectedOption.value, bucketCombo, flowCombo, selectBucket).fail(function () {
+            loadBuckets(selectedOption.value, bucketCombo, flowCombo, selectBucket, bucketCheck).fail(function () {
                 showNoBucketsAvailable();
             });
         }
@@ -744,7 +763,9 @@
             }]
         }).show();
 
-        loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, flowCombo, selectBucketImportVersion).done(function () {
+        loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, flowCombo, selectBucketImportVersion, function (bucketEntity) {
+            return true;
+        }).done(function () {
             // show the import dialog
             $('#import-flow-version-dialog').modal('setHeaderText', 'Import Version').modal('setButtonModel', [{
                 buttonText: 'Import',
@@ -904,23 +925,35 @@
      * @param selectedBucket
      */
     var selectBucketImportVersion = function (selectedBucket) {
-        // mark the flows as loading
-        $('#import-flow-version-name-combo').combo('destroy').combo({
-            options: [{
-                text: 'Loading flows...',
-                value: null,
-                optionClass: 'unset',
-                disabled: true
-            }]
-        });
-
         // clear the flow versions grid
         clearFlowVersionsGrid();
 
-        var selectedRegistry = $('#import-flow-version-registry-combo').combo('getSelectedOption');
+        if (nfCommon.isDefinedAndNotNull(selectedBucket.value)) {
+            // mark the flows as loading
+            $('#import-flow-version-name-combo').combo('destroy').combo({
+                options: [{
+                    text: 'Loading flows...',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
+
+            var selectedRegistry = $('#import-flow-version-registry-combo').combo('getSelectedOption');
 
-        // load the flows for the currently selected registry and bucket
-        loadFlows(selectedRegistry.value, selectedBucket.value, selectVersionedFlow);
+            // load the flows for the currently selected registry and bucket
+            loadFlows(selectedRegistry.value, selectedBucket.value, selectVersionedFlow);
+        } else {
+            // mark no flows available
+            $('#import-flow-version-name-combo').combo('destroy').combo({
+                options: [{
+                    text: 'No available flows',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
+        }
     };
 
     /**
@@ -988,7 +1021,22 @@
         var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance');
         if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) {
             var selected = importFlowVersionGrid.getSelectedRows();
-            return selected.length !== 1;
+
+            // if the version label is visible, this is a change version request so disable when
+            // the version that represents the current version is selected
+            if ($('#import-flow-version-label').is(':visible')) {
+                if (selected.length === 1) {
+                    var selectedFlow = importFlowVersionGrid.getDataItem(selected[0]);
+
+                    var currentVersion = parseInt($('#import-flow-version-label').text(), 10);
+                    return currentVersion === selectedFlow.version;
+                } else {
+                    return true;
+                }
+            } else {
+                // if importing, enable when a single row is selecting
+                return selected.length !== 1;
+            }
         } else {
             return true;
         }
@@ -1731,7 +1779,9 @@
                         // reposition the version label
                         $('#save-flow-version-label').css('margin-top', '0');
 
-                        loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, null, selectBucketSaveFlowVersion).done(function () {
+                        loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, null, selectBucketSaveFlowVersion, function (bucketEntity) {
+                            return bucketEntity.permissions.canWrite === true;
+                        }).done(function () {
                             deferred.resolve();
                         }).fail(function () {
                             deferred.reject();


[47/50] nifi git commit: NIFI-4436: Fixed bug that caused in Process Groups' names not to be fixed when reverting changes

Posted by bb...@apache.org.
NIFI-4436: Fixed bug that caused in Process Groups' names not to be fixed when reverting changes

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/63544c88
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/63544c88
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/63544c88

Branch: refs/heads/master
Commit: 63544c880ff173213f24db7bcf57fcf97a925556
Parents: fd18eeb
Author: Mark Payne <ma...@hotmail.com>
Authored: Wed Jan 3 09:45:57 2018 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Mon Jan 8 12:44:57 2018 -0500

----------------------------------------------------------------------
 .../src/main/java/org/apache/nifi/groups/StandardProcessGroup.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/63544c88/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 83468cf..870804d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -3470,7 +3470,7 @@ public final class StandardProcessGroup implements ProcessGroup {
                 added.findAllRemoteProcessGroups().stream().forEach(RemoteProcessGroup::initialize);
                 LOG.info("Added {} to {}", added, this);
             } else if (childCoordinates == null || updateDescendantVersionedGroups) {
-                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, updateName, updateDescendantVersionedGroups, variablesToSkip);
+                updateProcessGroup(childGroup, proposedChildGroup, componentIdSeed, updatedVersionedComponentIds, true, true, updateDescendantVersionedGroups, variablesToSkip);
                 LOG.info("Updated {}", childGroup);
             }