Add DCAE MOD design tool project 93/101393/2
authorAndrew Gauld <agauld@att.com>
Fri, 7 Feb 2020 15:00:39 +0000 (15:00 +0000)
committerAndrew Gauld <agauld@att.com>
Fri, 7 Feb 2020 20:37:14 +0000 (20:37 +0000)
Change-Id: I660b28ebfaa7e4b5f03a1df5fd17d126f58b7c14
Issue-ID: DCAEGEN2-1860
Signed-off-by: Andrew Gauld <agauld@att.com>
37 files changed:
mod/designtool/README.md [new file with mode: 0644]
mod/designtool/designtool-web/Dockerfile [new file with mode: 0644]
mod/designtool/designtool-web/pom.xml [new file with mode: 0644]
mod/designtool/designtool-web/sh/applypatches.sh [new file with mode: 0755]
mod/designtool/designtool-web/sh/common.sh [new file with mode: 0755]
mod/designtool/designtool-web/sh/start.sh [new file with mode: 0755]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/NiFi.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java [new file with mode: 0644]
mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/css/navigation.css [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/images/dcae-logo.png [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/jquery/dcae-mod.js [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/header/components/nf-ng-processor-component.js [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js [new file with mode: 0644]
mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js [new file with mode: 0644]
mod/designtool/nifi-war-to-jar/extract.sh [new file with mode: 0755]
mod/designtool/nifi-war-to-jar/pom.xml [new file with mode: 0644]
mod/designtool/pom.xml [new file with mode: 0644]

diff --git a/mod/designtool/README.md b/mod/designtool/README.md
new file mode 100644 (file)
index 0000000..11a9bda
--- /dev/null
@@ -0,0 +1,37 @@
+# DCAE MOD's Design tool
+
+## License
+
+Copyright 2020 AT&T Intellectual Property. All rights reserved.
+
+This file is licensed under the CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL LICENSE
+
+Full license text at https://creativecommons.org/licenses/by/4.0/legalcode
+
+
+## Description
+
+DCAE MOD's DCAE design tool is based on Nifi 1.9.2 with modifications
+made by the DCAE MOD team.
+
+## Development
+
+The designtool-web module contains the modified versions of Nifi files, along
+with a Dockerfile and a script (sh/applypatches.sh) for replacing them in
+the nifi Docker image, to produce the design tool Docker image.
+
+If the set of modified files changes, then the Dockerfile, the script, and
+potentially the pom.xml may require changes.
+
+In particular, note that the Nifi build creates 2 "bin" files, one for nifi
+itself and the other for the nifi-toolkit, which are expanded into separate
+directories in the nifi image.  Contained in the "bin" files are "nar" files,
+which contain "jar" and "war" files.  And, inside the nifi-web-ui "war" file
+are several "-all.js" and "-all.css" files, containing minified aggregations
+of the various js and css source files.  The applypatches script needs to
+appropriately patch these nar, war, jar, all.js, and all-css files (some of
+which also have gzipped versions).
+
+The nifi-war-to-jar module builds a jar archive from the classes in the
+nifi-web-api war archive, that the modified files in the designtool-web
+module can be compiled against.
diff --git a/mod/designtool/designtool-web/Dockerfile b/mod/designtool/designtool-web/Dockerfile
new file mode 100644 (file)
index 0000000..f4559ff
--- /dev/null
@@ -0,0 +1,77 @@
+# ============LICENSE_START=====================================================
+# Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+# ==============================================================================
+# Licensed 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.
+# ============LICENSE_END=======================================================
+
+FROM apache/nifi:${nifi.version} as original
+
+FROM openjdk:8 as artifactbase
+
+ENV NIFI_BASE_DIR /opt/nifi
+
+COPY --from=original $NIFI_BASE_DIR $NIFI_BASE_DIR
+
+ADD sh/ /tmp/patches
+
+COPY target/designtool-web-${project.version}.war /tmp/patches
+
+RUN bash /tmp/patches/applypatches.sh ${project.version} ${nifi.version}
+
+FROM openjdk:8-jre
+
+ARG UID=1000
+ARG GID=1000
+
+ENV NIFI_BASE_DIR /opt/nifi
+ENV NIFI_HOME ${NIFI_BASE_DIR}/nifi-current
+ENV NIFI_TOOLKIT_HOME ${NIFI_BASE_DIR}/nifi-toolkit-current
+ENV NIFI_PID_DIR=${NIFI_HOME}/run
+ENV NIFI_LOG_DIR=${NIFI_HOME}/logs
+
+# Setup NiFi user and create necessary directories
+RUN groupadd -g ${GID} nifi || groupmod -n nifi `getent group ${GID} | cut -d: -f1` \
+    && useradd --shell /bin/bash -u ${UID} -g ${GID} -m nifi \
+    && apt-get update \
+    && apt-get install -y jq xmlstarlet procps
+
+COPY --chown=nifi:nifi --from=artifactbase $NIFI_BASE_DIR $NIFI_BASE_DIR
+
+VOLUME ${NIFI_LOG_DIR} \
+       ${NIFI_HOME}/conf \
+       ${NIFI_HOME}/database_repository \
+       ${NIFI_HOME}/flowfile_repository \
+       ${NIFI_HOME}/content_repository \
+       ${NIFI_HOME}/provenance_repository \
+       ${NIFI_HOME}/state
+
+USER nifi
+
+# Clear nifi-env.sh in favour of configuring all environment variables in the Dockerfile
+RUN echo "#!/bin/sh\n" > $NIFI_HOME/bin/nifi-env.sh
+
+# Web HTTP(s) & Socket Site-to-Site Ports
+EXPOSE 8080 8443 10000
+
+WORKDIR ${NIFI_HOME}
+
+# Apply configuration and start NiFi
+#
+# We need to use the exec form to avoid running our command in a subshell and omitting signals,
+# thus being unable to shut down gracefully:
+# https://docs.docker.com/engine/reference/builder/#entrypoint
+#
+# Also we need to use relative path, because the exec form does not invoke a command shell,
+# thus normal shell processing does not happen:
+# https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example
+ENTRYPOINT ["../scripts/start.sh"]
diff --git a/mod/designtool/designtool-web/pom.xml b/mod/designtool/designtool-web/pom.xml
new file mode 100644 (file)
index 0000000..7cf0d8a
--- /dev/null
@@ -0,0 +1,255 @@
+<?xml version="1.0"?>
+<!--
+================================================================================
+Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+================================================================================
+Licensed 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.
+============LICENSE_END=========================================================
+
+-->
+<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.onap.dcaegen2.platform.mod</groupId>
+    <artifactId>designtool</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>designtool-web</artifactId>
+  <packaging>war</packaging>
+  <name>dcaegen2-platform-mod-designtool-web</name>
+  <properties>
+    <canvas.filter>canvas-min.properties</canvas.filter>
+  </properties>
+  <repositories>
+    <repository>
+      <id>jcenter</id>
+      <url>https://jcenter.bintray.com</url>
+      <snapshots>
+        <enabled>false</enabled>
+      </snapshots>
+      <releases>
+        <enabled>true</enabled>
+      </releases>
+    </repository>
+  </repositories>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.nifi</groupId>
+      <artifactId>nifi-framework-cluster</artifactId>
+      <version>${nifi.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.nifi</groupId>
+      <artifactId>nifi-ui-extension</artifactId>
+      <version>${nifi.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.nifi</groupId>
+      <artifactId>nifi-jetty</artifactId>
+      <version>${nifi.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.nifi</groupId>
+      <artifactId>nifi-documentation</artifactId>
+      <version>${nifi.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.nifi</groupId>
+      <artifactId>nifi-web-content-access</artifactId>
+      <version>${nifi.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jul-to-slf4j</artifactId>
+      <version>${org.slf4j.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.onap.dcaegen2.platform.mod</groupId>
+      <artifactId>nifi-war-to-jar</artifactId>
+      <version>${project.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-server</artifactId>
+      <version>${jetty.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-annotations</artifactId>
+      <version>${jetty.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-deploy</artifactId>
+      <version>${jetty.version}</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <filters>
+      <filter>src/main/resources/filters/${canvas.filter}</filter>
+    </filters>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.jetty</groupId>
+        <artifactId>jetty-jspc-maven-plugin</artifactId>
+        <version>${jetty.version}</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jspc</goal>
+            </goals>
+            <configuration>
+              <keepSources>true</keepSources>
+              <useProvidedScope>true</useProvidedScope>
+              <excludes>
+                **/canvas.jsp
+              </excludes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>net.alchim31.maven</groupId>
+        <artifactId>yuicompressor-maven-plugin</artifactId>
+        <version>1.5.1</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compress</goal>
+            </goals>
+            <configuration>
+              <sourceDirectory>src/main/webapp</sourceDirectory>
+              <outputDirectory>${staging.dir}</outputDirectory>
+              <nomunge>false</nomunge>
+              <jswarn>false</jswarn>
+              <nosuffix>true</nosuffix>
+              <gzip>true</gzip>
+              <aggregations>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}//nf-ng-breadcrumbs-controller-min.js</output>
+                  <includes>
+                    <include>${staging.dir}/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js</include>
+                  </includes>
+                </aggregation>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}//nf-ng-processor-component-min.js</output>
+                  <includes>
+                    <include>${staging.dir}/js/nf/canvas/header/components/nf-ng-processor-component.js</include>
+                  </includes>
+               </aggregation>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}//nf-connection-configuration-min.js</output>
+                  <includes>
+                    <include>${staging.dir}/js/nf/canvas/nf-connection-configuration.js</include>
+                  </includes>
+               </aggregation>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}//nf-flow-version-min.js</output>
+                  <includes>
+                    <include>${staging.dir}/js/nf/canvas/nf-flow-version.js</include>
+                  </includes>
+               </aggregation>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}//nf-process-group-min.js</output>
+                  <includes>
+                    <include>${staging.dir}/js/nf/canvas/nf-process-group.js</include>
+                  </includes>
+                </aggregation>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}//nf-settings-min.js</output>
+                  <includes>
+                    <include>${staging.dir}/js/nf/canvas/nf-settings.js</include>
+                  </includes>
+                </aggregation>
+                <aggregation>
+                  <insertNewLine>true</insertNewLine>
+                  <output>${project.build.directory}/${project.build.finalName}/navigation-min.css</output>
+                  <includes>
+                    <include>${staging.dir}/css/navigation.css</include>
+                  </includes>
+                </aggregation>
+              </aggregations>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+       <version>3.2.1</version>
+        <configuration>
+          <webResources>
+            <resource>
+              <directory>src/main/webapp/WEB-INF/pages</directory>
+              <targetPath>WEB-INF/pages</targetPath>
+              <includes>
+                <include>canvas.jsp</include>
+              </includes>
+              <filtering>true</filtering>
+            </resource>
+          </webResources>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>io.fabric8</groupId>
+        <artifactId>docker-maven-plugin</artifactId>
+        <version>${docker.fabric.version}</version>
+        <configuration>
+          <verbose>true</verbose>
+          <pullRegistry>${docker.pull.registry}</pullRegistry>
+          <pushRegistry>${docker.push.registry}</pushRegistry>
+          <images>
+            <image>
+              <name>onap/${project.groupId}.${project.artifactId}</name>
+              <registry>${onap.nexus.dockerregistry.daily}</registry>
+              <build>
+                <contextDir>${project.basedir}</contextDir>
+                <tags>
+                  <tag>latest</tag>
+                  <tag>${project.version}</tag>
+                  <tag>${project.version}-${maven.build.timestamp}Z</tag>
+                </tags>
+              </build>
+            </image>
+          </images>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>build</goal>
+              <goal>push</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/mod/designtool/designtool-web/sh/applypatches.sh b/mod/designtool/designtool-web/sh/applypatches.sh
new file mode 100755 (executable)
index 0000000..47de89c
--- /dev/null
@@ -0,0 +1,107 @@
+#!/bin/bash
+# ============LICENSE_START=====================================================
+# Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+# ==============================================================================
+# Licensed 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.
+# ============LICENSE_END=======================================================
+
+set -eufx -o pipefail
+
+PATCH_BINARY=/tmp/patches/designtool-web-$1.war
+NIFI_VERSION=$2
+PATCHES=/tmp/patches
+TARGETS=/tmp/targets
+mkdir -p $PATCHES $TARGETS
+# extract patches
+cd $PATCHES
+jar xf $PATCH_BINARY
+rm $PATCH_BINARY
+# extract jars and wars to be patched
+cd $TARGETS
+jar xf $NIFI_BASE_DIR/nifi-current/lib/nifi-framework-nar-$NIFI_VERSION.nar \
+    META-INF/bundled-dependencies/nifi-framework-nar-loading-utils-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-jetty-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-web-api-$NIFI_VERSION.war \
+    META-INF/bundled-dependencies/nifi-web-ui-$NIFI_VERSION.war
+# patch jar files
+cd $PATCHES/WEB-INF/classes
+set +f
+jar uf $NIFI_BASE_DIR/nifi-toolkit-current/lib/nifi-client-dto-$NIFI_VERSION.jar \
+    org/apache/nifi/web/api/dto/FlowConfigurationDTO*.class
+jar uf $TARGETS/META-INF/bundled-dependencies/nifi-jetty-$NIFI_VERSION.jar \
+    org/apache/nifi/web/server/JettyServer*.class
+jar uf $NIFI_BASE_DIR/nifi-current/lib/nifi-properties-$NIFI_VERSION.jar \
+    org/apache/nifi/util/NiFiProperties*.class
+jar uf $NIFI_BASE_DIR/nifi-current/lib/nifi-runtime-$NIFI_VERSION.jar \
+    org/apache/nifi/NiFi*.class
+jar uf $NIFI_BASE_DIR/nifi-toolkit-current/lib/nifi-framework-core-api-$NIFI_VERSION.jar \
+    org/apache/nifi/controller/AbstractPort*.class
+jar uf $TARGETS/META-INF/bundled-dependencies/nifi-framework-nar-loading-utils-$NIFI_VERSION.jar \
+    org/apache/nifi/nar/DCAEClassLoaders*.class \
+    org/apache/nifi/nar/DCAEAutoLoader*.class
+# patch war files
+cd $PATCHES
+jar uf $TARGETS/META-INF/bundled-dependencies/nifi-web-api-$NIFI_VERSION.war \
+    WEB-INF/classes/org/apache/nifi/web/StandardNiFiServiceFacade*.class \
+    WEB-INF/classes/org/apache/nifi/web/api/dto/DtoFactory*.class \
+    WEB-INF/classes/org/apache/nifi/web/dao/impl/StandardConnectionDAO*.class
+set -f
+jar xf $TARGETS/META-INF/bundled-dependencies/nifi-web-ui-$NIFI_VERSION.war \
+    css/nf-canvas-all.css \
+    js/nf/canvas/nf-canvas-all.js \
+    js/nf/summary/nf-summary-all.js
+rm -f \
+    css/nf-canvas-all.css.gz \
+    js/nf/canvas/nf-canvas-all.js.gz \
+    js/nf/summary/nf-summary-all.js.gz
+sed -i \
+    -e '/graph-controls/{r navigation-min.css' -e 'd}' \
+    css/nf-canvas-all.css
+sed -i \
+    -e '/process-group-up-to-date/{r nf-process-group-min.js' -e 'd}' \
+    -e '/div.available-relationship/{r nf-connection-configuration-min.js' -e 'd}' \
+    -e '/nf.FlowVerison/{r nf-flow-version-min.js' -e 'd}' \
+    -e '/controllerConfig/{r nf-settings-min.js' -e 'd}' \
+    -e '/this.breadcrumbs/{r nf-ng-breadcrumbs-controller-min.js' -e 'd}' \
+    -e '/processor-types-table/{r nf-ng-processor-component-min.js' -e 'd}' \
+    js/nf/canvas/nf-canvas-all.js
+sed -i \
+    -e '/controllerConfig/{r nf-settings-min.js' -e 'd}' \
+    js/nf/summary/nf-summary-all.js
+gzip -k \
+    css/nf-canvas-all.css \
+    js/nf/canvas/nf-canvas-all.js \
+    js/nf/summary/nf-summary-all.js
+jar uf $TARGETS/META-INF/bundled-dependencies/nifi-web-ui-$NIFI_VERSION.war \
+    $(find WEB-INF/classes/org/apache/jsp/WEB_002dINF WEB-INF/pages WEB-INF/partials css js images -type f -print)
+# patch scripts
+cp common.sh start.sh $NIFI_BASE_DIR/scripts/
+# patch nar files
+cd $TARGETS
+cp $NIFI_BASE_DIR/nifi-toolkit-current/lib/nifi-client-dto-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-client-dto-$NIFI_VERSION.jar
+jar uf $NIFI_BASE_DIR/nifi-current/lib/nifi-site-to-site-reporting-nar-$NIFI_VERSION.nar \
+    META-INF/bundled-dependencies/nifi-client-dto-$NIFI_VERSION.jar
+cp $NIFI_BASE_DIR/nifi-toolkit-current/lib/nifi-framework-core-api-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-framework-core-api-$NIFI_VERSION.jar
+jar uf $NIFI_BASE_DIR/nifi-current/lib/nifi-framework-nar-$NIFI_VERSION.nar \
+    META-INF/bundled-dependencies/nifi-client-dto-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-framework-core-api-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-framework-nar-loading-utils-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-jetty-$NIFI_VERSION.jar \
+    META-INF/bundled-dependencies/nifi-web-api-$NIFI_VERSION.war \
+    META-INF/bundled-dependencies/nifi-web-ui-$NIFI_VERSION.war
+cp $NIFI_BASE_DIR/nifi-current/lib/nifi-properties-$NIFI_VERSION.jar \
+    $NIFI_BASE_DIR/nifi-toolkit-current/lib/nifi-properties-$NIFI_VERSION.jar
+echo Success
+exit 0
diff --git a/mod/designtool/designtool-web/sh/common.sh b/mod/designtool/designtool-web/sh/common.sh
new file mode 100755 (executable)
index 0000000..542b777
--- /dev/null
@@ -0,0 +1,43 @@
+#!/bin/sh -e
+#    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.
+
+#
+#    Modifications to the original nifi code for the ONAP project are made
+#    available under the Apache License, Version 2.0
+#
+
+# 1 - value to search for
+# 2 - value to replace
+# 3 - file to perform replacement inline
+prop_replace () {
+  target_file=${3:-${nifi_props_file}}
+  echo 'replacing target file ' ${target_file}
+  sed -i -e "s|^$1=.*$|$1=$2|"  ${target_file}
+}
+
+# 1 - property name
+# 2 - property value
+# 3 - file to perform replacement inline
+prop_append () {
+  target_file=${3:-${nifi_props_file}}
+  echo 'appending target file ' ${target_file}
+  sed -i "$ a $1=$2"  ${target_file}
+}
+
+# NIFI_HOME is defined by an ENV command in the backing Dockerfile
+export nifi_props_file=${NIFI_HOME}/conf/nifi.properties
+export nifi_toolkit_props_file=${HOME}/.nifi-cli.nifi.properties
+export hostname=$(hostname)
diff --git a/mod/designtool/designtool-web/sh/start.sh b/mod/designtool/designtool-web/sh/start.sh
new file mode 100755 (executable)
index 0000000..8658983
--- /dev/null
@@ -0,0 +1,97 @@
+#!/bin/sh -e
+
+#    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.
+
+#
+#    Modifications to the original nifi code for the ONAP project are made
+#    available under the Apache License, Version 2.0
+#
+
+scripts_dir='/opt/nifi/scripts'
+
+[ -f "${scripts_dir}/common.sh" ] && . "${scripts_dir}/common.sh"
+
+# Establish baseline properties
+prop_replace 'nifi.web.http.port'               "${NIFI_WEB_HTTP_PORT:-8080}"
+prop_replace 'nifi.web.http.host'               "${NIFI_WEB_HTTP_HOST:-$HOSTNAME}"
+prop_replace 'nifi.remote.input.host'           "${NIFI_REMOTE_INPUT_HOST:-$HOSTNAME}"
+prop_replace 'nifi.remote.input.socket.port'    "${NIFI_REMOTE_INPUT_SOCKET_PORT:-10000}"
+prop_replace 'nifi.remote.input.secure'         'false'
+
+# Set nifi-toolkit properties files and baseUrl
+"${scripts_dir}/toolkit.sh"
+prop_replace 'baseUrl' "http://${NIFI_WEB_HTTP_HOST:-$HOSTNAME}:${NIFI_WEB_HTTP_PORT:-8080}" ${nifi_toolkit_props_file}
+
+prop_replace 'nifi.variable.registry.properties'    "${NIFI_VARIABLE_REGISTRY_PROPERTIES:-}"
+prop_replace 'nifi.cluster.is.node'                         "${NIFI_CLUSTER_IS_NODE:-false}"
+prop_replace 'nifi.cluster.node.address'                    "${NIFI_CLUSTER_ADDRESS:-$HOSTNAME}"
+prop_replace 'nifi.cluster.node.protocol.port'              "${NIFI_CLUSTER_NODE_PROTOCOL_PORT:-}"
+prop_replace 'nifi.cluster.node.protocol.threads'           "${NIFI_CLUSTER_NODE_PROTOCOL_THREADS:-10}"
+prop_replace 'nifi.cluster.node.protocol.max.threads'       "${NIFI_CLUSTER_NODE_PROTOCOL_MAX_THREADS:-50}"
+prop_replace 'nifi.zookeeper.connect.string'                "${NIFI_ZK_CONNECT_STRING:-}"
+prop_replace 'nifi.zookeeper.root.node'                     "${NIFI_ZK_ROOT_NODE:-/nifi}"
+prop_replace 'nifi.cluster.flow.election.max.wait.time'     "${NIFI_ELECTION_MAX_WAIT:-5 mins}"
+prop_replace 'nifi.cluster.flow.election.max.candidates'    "${NIFI_ELECTION_MAX_CANDIDATES:-}"
+prop_replace 'nifi.web.proxy.context.path'                  "${NIFI_WEB_PROXY_CONTEXT_PATH:-}"
+
+# REVIEW: Could not figure out how the nifi.properties file gets generated so
+# replace value conditionally if the property name exists otherwise append
+if grep -q 'nifi.dcae.jars.index.url' $nifi_props_file
+then
+    prop_replace 'nifi.dcae.jars.index.url'                     "${NIFI_DCAE_JARS_INDEX_URL:-http://genprocessor-http/nifi-jars/}"
+else
+    prop_append 'nifi.dcae.jars.index.url'                     "${NIFI_DCAE_JARS_INDEX_URL:-http://genprocessor-http/nifi-jars/}"
+fi
+
+if grep -q 'nifi.ui.dcae.distibutor.api.url' $nifi_props_file
+then
+    prop_replace 'nifi.ui.dcae.distibutor.api.url' "${NIFI_DCAE_DISTRIBUTOR_API_URL:-http://distributor-api}"
+else
+    prop_append 'nifi.ui.dcae.distibutor.api.url' "${NIFI_DCAE_DISTRIBUTOR_API_URL:-http://distributor-api}"
+fi
+
+. "${scripts_dir}/update_cluster_state_management.sh"
+
+# Check if we are secured or unsecured
+case ${AUTH} in
+    tls)
+        echo 'Enabling Two-Way SSL user authentication'
+        . "${scripts_dir}/secure.sh"
+        ;;
+    ldap)
+        echo 'Enabling LDAP user authentication'
+        # Reference ldap-provider in properties
+        prop_replace 'nifi.security.user.login.identity.provider' 'ldap-provider'
+
+        . "${scripts_dir}/secure.sh"
+        . "${scripts_dir}/update_login_providers.sh"
+        ;;
+    *)
+        if [ ! -z "${NIFI_WEB_PROXY_HOST}" ]; then
+            echo 'NIFI_WEB_PROXY_HOST was set but NiFi is not configured to run in a secure mode.  Will not update nifi.web.proxy.host.'
+        fi
+        ;;
+esac
+
+# Continuously provide logs so that 'docker logs' can    produce them
+tail -F "${NIFI_HOME}/logs/nifi-app.log" &
+"${NIFI_HOME}/bin/nifi.sh" run &
+nifi_pid="$!"
+
+trap "echo Received trapped signal, beginning shutdown...;" KILL TERM HUP INT EXIT;
+
+echo NiFi running with PID ${nifi_pid}.
+wait ${nifi_pid}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/NiFi.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/NiFi.java
new file mode 100644 (file)
index 0000000..0b033db
--- /dev/null
@@ -0,0 +1,446 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi;
+
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.nar.ExtensionMapping;
+import org.apache.nifi.nar.NarClassLoaders;
+import org.apache.nifi.nar.NarClassLoadersHolder;
+import org.apache.nifi.nar.NarUnpacker;
+import org.apache.nifi.nar.SystemBundle;
+import org.apache.nifi.util.FileUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class NiFi {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(NiFi.class);
+    private static final String KEY_FILE_FLAG = "-K";
+    private final NiFiServer nifiServer;
+    private final BootstrapListener bootstrapListener;
+
+    public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.bootstrap.listen.port";
+    private volatile boolean shutdown = false;
+
+    public NiFi(final NiFiProperties properties)
+            throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+
+        this(properties, ClassLoader.getSystemClassLoader());
+
+    }
+
+    public NiFi(final NiFiProperties properties, ClassLoader rootClassLoader)
+            throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+
+        // There can only be one krb5.conf for the overall Java process so set this globally during
+        // start up so that processors and our Kerberos authentication code don't have to set this
+        final File kerberosConfigFile = properties.getKerberosConfigurationFile();
+        if (kerberosConfigFile != null) {
+            final String kerberosConfigFilePath = kerberosConfigFile.getAbsolutePath();
+            LOGGER.info("Setting java.security.krb5.conf to {}", new Object[]{kerberosConfigFilePath});
+            System.setProperty("java.security.krb5.conf", kerberosConfigFilePath);
+        }
+
+        setDefaultUncaughtExceptionHandler();
+
+        // register the shutdown hook
+        addShutdownHook();
+
+        final String bootstrapPort = System.getProperty(BOOTSTRAP_PORT_PROPERTY);
+        if (bootstrapPort != null) {
+            try {
+                final int port = Integer.parseInt(bootstrapPort);
+
+                if (port < 1 || port > 65535) {
+                    throw new RuntimeException("Failed to start NiFi because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
+                }
+
+                bootstrapListener = new BootstrapListener(this, port);
+                bootstrapListener.start();
+            } catch (final NumberFormatException nfe) {
+                throw new RuntimeException("Failed to start NiFi because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
+            }
+        } else {
+            LOGGER.info("NiFi started without Bootstrap Port information provided; will not listen for requests from Bootstrap");
+            bootstrapListener = null;
+        }
+
+        // delete the web working dir - if the application does not start successfully
+        // the web app directories might be in an invalid state. when this happens
+        // jetty will not attempt to re-extract the war into the directory. by removing
+        // the working directory, we can be assured that it will attempt to extract the
+        // war every time the application starts.
+        File webWorkingDir = properties.getWebWorkingDirectory();
+        FileUtils.deleteFilesInDirectory(webWorkingDir, null, LOGGER, true, true);
+        FileUtils.deleteFile(webWorkingDir, LOGGER, 3);
+
+        detectTimingIssues();
+
+        // redirect JUL log events
+        initLogging();
+
+        final Bundle systemBundle = SystemBundle.create(properties);
+
+        // expand the nars
+        final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties, systemBundle);
+
+        // load the extensions classloaders
+        NarClassLoaders narClassLoaders = NarClassLoadersHolder.getInstance();
+
+        narClassLoaders.init(rootClassLoader,
+                properties.getFrameworkWorkingDirectory(), properties.getExtensionsWorkingDirectory());
+
+        // load the framework classloader
+        final ClassLoader frameworkClassLoader = narClassLoaders.getFrameworkBundle().getClassLoader();
+        if (frameworkClassLoader == null) {
+            throw new IllegalStateException("Unable to find the framework NAR ClassLoader.");
+        }
+
+        final Set<Bundle> narBundles = narClassLoaders.getBundles();
+
+        // load the server from the framework classloader
+        Thread.currentThread().setContextClassLoader(frameworkClassLoader);
+        Class<?> jettyServer = Class.forName("org.apache.nifi.web.server.JettyServer", true, frameworkClassLoader);
+        Constructor<?> jettyConstructor = jettyServer.getConstructor(NiFiProperties.class, Set.class);
+
+        final long startTime = System.nanoTime();
+        nifiServer = (NiFiServer) jettyConstructor.newInstance(properties, narBundles);
+        nifiServer.setExtensionMapping(extensionMapping);
+        nifiServer.setBundles(systemBundle, narBundles);
+
+        if (shutdown) {
+            LOGGER.info("NiFi has been shutdown via NiFi Bootstrap. Will not start Controller");
+        } else {
+            nifiServer.start();
+
+            if (bootstrapListener != null) {
+                bootstrapListener.sendStartedStatus(true);
+            }
+
+            final long duration = System.nanoTime() - startTime;
+            LOGGER.info("Controller initialization took " + duration + " nanoseconds "
+                    + "(" + (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS) + " seconds).");
+        }
+    }
+
+    protected void setDefaultUncaughtExceptionHandler() {
+        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+            @Override
+            public void uncaughtException(final Thread t, final Throwable e) {
+                LOGGER.error("An Unknown Error Occurred in Thread {}: {}", t, e.toString());
+                LOGGER.error("", e);
+            }
+        });
+    }
+
+    protected void addShutdownHook() {
+        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+            @Override
+            public void run() {
+                // shutdown the jetty server
+                shutdownHook();
+            }
+        }));
+    }
+
+    protected void initLogging() {
+        SLF4JBridgeHandler.removeHandlersForRootLogger();
+        SLF4JBridgeHandler.install();
+    }
+
+    private static ClassLoader createBootstrapClassLoader() {
+        //Get list of files in bootstrap folder
+        final List<URL> urls = new ArrayList<>();
+        try {
+            Files.list(Paths.get("lib/bootstrap")).forEach(p -> {
+                try {
+                    urls.add(p.toUri().toURL());
+                } catch (final MalformedURLException mef) {
+                    LOGGER.warn("Unable to load " + p.getFileName() + " due to " + mef, mef);
+                }
+            });
+        } catch (IOException ioe) {
+            LOGGER.warn("Unable to access lib/bootstrap to create bootstrap classloader", ioe);
+        }
+        //Create the bootstrap classloader
+        return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader());
+    }
+
+    protected void shutdownHook() {
+        try {
+            shutdown();
+        } catch (final Throwable t) {
+            LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated due to " + t);
+        }
+    }
+
+    protected void shutdown() {
+        this.shutdown = true;
+
+        LOGGER.info("Initiating shutdown of Jetty web server...");
+        if (nifiServer != null) {
+            nifiServer.stop();
+        }
+        if (bootstrapListener != null) {
+            bootstrapListener.stop();
+        }
+        LOGGER.info("Jetty web server shutdown completed (nicely or otherwise).");
+    }
+
+    /**
+     * Determine if the machine we're running on has timing issues.
+     */
+    private void detectTimingIssues() {
+        final int minRequiredOccurrences = 25;
+        final int maxOccurrencesOutOfRange = 15;
+        final AtomicLong lastTriggerMillis = new AtomicLong(System.currentTimeMillis());
+
+        final ScheduledExecutorService service = Executors.newScheduledThreadPool(1, new ThreadFactory() {
+            private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
+
+            @Override
+            public Thread newThread(final Runnable r) {
+                final Thread t = defaultFactory.newThread(r);
+                t.setDaemon(true);
+                t.setName("Detect Timing Issues");
+                return t;
+            }
+        });
+
+        final AtomicInteger occurrencesOutOfRange = new AtomicInteger(0);
+        final AtomicInteger occurrences = new AtomicInteger(0);
+        final Runnable command = new Runnable() {
+            @Override
+            public void run() {
+                final long curMillis = System.currentTimeMillis();
+                final long difference = curMillis - lastTriggerMillis.get();
+                final long millisOff = Math.abs(difference - 2000L);
+                occurrences.incrementAndGet();
+                if (millisOff > 500L) {
+                    occurrencesOutOfRange.incrementAndGet();
+                }
+                lastTriggerMillis.set(curMillis);
+            }
+        };
+
+        final ScheduledFuture<?> future = service.scheduleWithFixedDelay(command, 2000L, 2000L, TimeUnit.MILLISECONDS);
+
+        final TimerTask timerTask = new TimerTask() {
+            @Override
+            public void run() {
+                future.cancel(true);
+                service.shutdownNow();
+
+                if (occurrences.get() < minRequiredOccurrences || occurrencesOutOfRange.get() > maxOccurrencesOutOfRange) {
+                    LOGGER.warn("NiFi has detected that this box is not responding within the expected timing interval, which may cause "
+                            + "Processors to be scheduled erratically. Please see the NiFi documentation for more information.");
+                }
+            }
+        };
+        final Timer timer = new Timer(true);
+        timer.schedule(timerTask, 60000L);
+    }
+
+    /**
+     * Main entry point of the application.
+     *
+     * @param args things which are ignored
+     */
+    public static void main(String[] args) {
+        LOGGER.info("Launching NiFi...");
+        try {
+            NiFiProperties properties = convertArgumentsToValidatedNiFiProperties(args);
+            new NiFi(properties);
+        } catch (final Throwable t) {
+            LOGGER.error("Failure to launch NiFi due to " + t, t);
+        }
+    }
+
+    protected static NiFiProperties convertArgumentsToValidatedNiFiProperties(String[] args) {
+        final ClassLoader bootstrap = createBootstrapClassLoader();
+        NiFiProperties properties = initializeProperties(args, bootstrap);
+        properties.validate();
+        return properties;
+    }
+
+    private static NiFiProperties initializeProperties(final String[] args, final ClassLoader boostrapLoader) {
+        // Try to get key
+        // If key doesn't exist, instantiate without
+        // Load properties
+        // If properties are protected and key missing, throw RuntimeException
+
+        final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+        final String key;
+        try {
+            key = loadFormattedKey(args);
+            // The key might be empty or null when it is passed to the loader
+        } catch (IllegalArgumentException e) {
+            final String msg = "The bootstrap process did not provide a valid key";
+            throw new IllegalArgumentException(msg, e);
+        }
+        Thread.currentThread().setContextClassLoader(boostrapLoader);
+
+        try {
+            final Class<?> propsLoaderClass = Class.forName("org.apache.nifi.properties.NiFiPropertiesLoader", true, boostrapLoader);
+            final Method withKeyMethod = propsLoaderClass.getMethod("withKey", String.class);
+            final Object loaderInstance = withKeyMethod.invoke(null, key);
+            final Method getMethod = propsLoaderClass.getMethod("get");
+            final NiFiProperties properties = (NiFiProperties) getMethod.invoke(loaderInstance);
+            LOGGER.info("Loaded {} properties", properties.size());
+            return properties;
+        } catch (InvocationTargetException wrappedException) {
+            final String msg = "There was an issue decrypting protected properties";
+            throw new IllegalArgumentException(msg, wrappedException.getCause() == null ? wrappedException : wrappedException.getCause());
+        } catch (final IllegalAccessException | NoSuchMethodException | ClassNotFoundException reex) {
+            final String msg = "Unable to access properties loader in the expected manner - apparent classpath or build issue";
+            throw new IllegalArgumentException(msg, reex);
+        } catch (final RuntimeException e) {
+            final String msg = "There was an issue decrypting protected properties";
+            throw new IllegalArgumentException(msg, e);
+        } finally {
+            Thread.currentThread().setContextClassLoader(contextClassLoader);
+        }
+    }
+
+    private static String loadFormattedKey(String[] args) {
+        String key = null;
+        List<String> parsedArgs = parseArgs(args);
+        // Check if args contain protection key
+        if (parsedArgs.contains(KEY_FILE_FLAG)) {
+            key = getKeyFromKeyFileAndPrune(parsedArgs);
+            // Format the key (check hex validity and remove spaces)
+            key = formatHexKey(key);
+
+        }
+
+        if (null == key) {
+            return "";
+        } else if (!isHexKeyValid(key)) {
+            throw new IllegalArgumentException("The key was not provided in valid hex format and of the correct length");
+        } else {
+            return key;
+        }
+    }
+
+    private static String getKeyFromKeyFileAndPrune(List<String> parsedArgs) {
+        String key = null;
+        LOGGER.debug("The bootstrap process provided the " + KEY_FILE_FLAG + " flag");
+        int i = parsedArgs.indexOf(KEY_FILE_FLAG);
+        if (parsedArgs.size() <= i + 1) {
+            LOGGER.error("The bootstrap process passed the {} flag without a filename", KEY_FILE_FLAG);
+            throw new IllegalArgumentException("The bootstrap process provided the " + KEY_FILE_FLAG + " flag but no key");
+        }
+        try {
+          String passwordfile_path = parsedArgs.get(i + 1);
+          // Slurp in the contents of the file:
+          byte[] encoded = Files.readAllBytes(Paths.get(passwordfile_path));
+          key = new String(encoded,StandardCharsets.UTF_8);
+          if (0 == key.length())
+            throw new IllegalArgumentException("Key in keyfile " + passwordfile_path + " yielded an empty key");
+
+          LOGGER.info("Now overwriting file in "+passwordfile_path);
+
+          // Overwrite the contents of the file (to avoid littering file system
+          // unlinked with key material):
+          File password_file = new File(passwordfile_path);
+          FileWriter overwriter = new FileWriter(password_file,false);
+
+          // Construe a random pad:
+          Random r = new Random();
+          StringBuffer sb = new StringBuffer();
+          // Note on correctness: this pad is longer, but equally sufficient.
+          while(sb.length() < encoded.length){
+            sb.append(Integer.toHexString(r.nextInt()));
+          }
+          String pad = sb.toString();
+          LOGGER.info("Overwriting key material with pad: "+pad);
+          overwriter.write(pad);
+          overwriter.close();
+
+          LOGGER.info("Removing/unlinking file: "+passwordfile_path);
+          password_file.delete();
+
+        } catch (IOException e) {
+          LOGGER.error("Caught IOException while retrieving the "+KEY_FILE_FLAG+"-passed keyfile; aborting: "+e.toString());
+          System.exit(1);
+        }
+
+        LOGGER.info("Read property protection key from key file provided by bootstrap process");
+        return key;
+    }
+
+    private static List<String> parseArgs(String[] args) {
+        List<String> parsedArgs = new ArrayList<>(Arrays.asList(args));
+        for (int i = 0; i < parsedArgs.size(); i++) {
+            if (parsedArgs.get(i).startsWith(KEY_FILE_FLAG + " ")) {
+                String[] split = parsedArgs.get(i).split(" ", 2);
+                parsedArgs.set(i, split[0]);
+                parsedArgs.add(i + 1, split[1]);
+                break;
+            }
+        }
+        return parsedArgs;
+    }
+
+    private static String formatHexKey(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            return "";
+        }
+        return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase();
+    }
+
+    private static boolean isHexKeyValid(String key) {
+        if (key == null || key.trim().isEmpty()) {
+            return false;
+        }
+        // Key length is in "nibbles" (i.e. one hex char = 4 bits)
+        return Arrays.asList(128, 196, 256).contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$");
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java
new file mode 100644 (file)
index 0000000..6023fc2
--- /dev/null
@@ -0,0 +1,675 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.controller;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+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.components.ValidationResult;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.ConnectableType;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.connectable.Position;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.ProcessSessionFactory;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.util.CharacterFilterUtils;
+import org.apache.nifi.util.FormatUtils;
+
+import java.util.ArrayList;
+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;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import static java.util.Objects.requireNonNull;
+
+public abstract class AbstractPort implements Port {
+
+    public static final Relationship PORT_RELATIONSHIP = new Relationship.Builder()
+            .description("The relationship through which all Flow Files are transferred")
+            .name("")
+            .build();
+
+    public static final long MINIMUM_PENALIZATION_MILLIS = 0L;
+    public static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS;
+
+    public static final long MINIMUM_YIELD_MILLIS = 0L;
+    public static final long DEFAULT_YIELD_PERIOD = 10000L;
+    public static final TimeUnit DEFAULT_YIELD_TIME_UNIT = TimeUnit.MILLISECONDS;
+
+    private final List<Relationship> relationships;
+
+    private final String id;
+    private final ConnectableType type;
+    private final AtomicReference<String> name;
+    private final AtomicReference<Position> position;
+    private final AtomicReference<String> comments;
+    private final AtomicReference<ProcessGroup> processGroup;
+    private final AtomicBoolean lossTolerant;
+    private final AtomicReference<ScheduledState> scheduledState;
+    private final AtomicInteger concurrentTaskCount;
+    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;
+
+    private final Set<Connection> outgoingConnections;
+    private final List<Connection> incomingConnections;
+
+    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
+    private final Lock readLock = rwLock.readLock();
+    private final Lock writeLock = rwLock.writeLock();
+
+    public AbstractPort(final String id, final String name, final ProcessGroup processGroup, final ConnectableType type, final ProcessScheduler scheduler) {
+        this.id = requireNonNull(id);
+        this.name = new AtomicReference<>(requireNonNull(name));
+        position = new AtomicReference<>(new Position(0D, 0D));
+        outgoingConnections = new HashSet<>();
+        incomingConnections = new ArrayList<>();
+        comments = new AtomicReference<>();
+        lossTolerant = new AtomicBoolean(false);
+        concurrentTaskCount = new AtomicInteger(1);
+        processScheduler = scheduler;
+
+        final List<Relationship> relationshipList = new ArrayList<>();
+        relationshipList.add(PORT_RELATIONSHIP);
+        relationships = Collections.unmodifiableList(relationshipList);
+        this.processGroup = new AtomicReference<>(processGroup);
+        this.type = type;
+        penalizationPeriod = new AtomicReference<>("30 sec");
+        yieldPeriod = new AtomicReference<>("1 sec");
+        yieldExpiration = new AtomicLong(0L);
+        schedulingPeriod = new AtomicReference<>("0 millis");
+        schedulingNanos = new AtomicLong(MINIMUM_SCHEDULING_NANOS);
+        scheduledState = new AtomicReference<>(ScheduledState.STOPPED);
+    }
+
+    @Override
+    public String getIdentifier() {
+        return id;
+    }
+
+    @Override
+    public String getProcessGroupIdentifier() {
+        final ProcessGroup procGroup = getProcessGroup();
+        return procGroup == null ? null : procGroup.getIdentifier();
+    }
+
+    @Override
+    public String getName() {
+        return name.get();
+    }
+
+    @Override
+    public void setName(final String name) {
+        if (this.name.get().equals(name)) {
+            return;
+        }
+
+        final ProcessGroup parentGroup = this.processGroup.get();
+        if (getConnectableType() == ConnectableType.INPUT_PORT) {
+            if (parentGroup.getInputPortByName(name) != null) {
+                throw new IllegalStateException("The requested new port name is not available");
+            }
+        } else if (getConnectableType() == ConnectableType.OUTPUT_PORT) {
+            if (parentGroup.getOutputPortByName(name) != null) {
+                throw new IllegalStateException("The requested new port name is not available");
+            }
+        }
+
+        this.name.set(name);
+    }
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        return getProcessGroup();
+    }
+
+    @Override
+    public Resource getResource() {
+        final ResourceType resourceType = ConnectableType.INPUT_PORT.equals(getConnectableType()) ? ResourceType.InputPort : ResourceType.OutputPort;
+        return ResourceFactory.getComponentResource(resourceType, getIdentifier(), getName());
+    }
+
+    @Override
+    public ProcessGroup getProcessGroup() {
+        return processGroup.get();
+    }
+
+    @Override
+    public void setProcessGroup(final ProcessGroup newGroup) {
+        this.processGroup.set(newGroup);
+    }
+
+    @Override
+    public String getComments() {
+        return comments.get();
+    }
+
+    @Override
+    public void setComments(final String comments) {
+        this.comments.set(CharacterFilterUtils.filterInvalidXmlCharacters(comments));
+    }
+
+    @Override
+    public Collection<Relationship> getRelationships() {
+        return relationships;
+    }
+
+    @Override
+    public Relationship getRelationship(final String relationshipName) {
+        if (PORT_RELATIONSHIP.getName().equals(relationshipName)) {
+            return PORT_RELATIONSHIP;
+        }
+        return null;
+    }
+
+    @Override
+    public void addConnection(final Connection connection) throws IllegalArgumentException {
+        writeLock.lock();
+        try {
+            if (!requireNonNull(connection).getSource().equals(this)) {
+                if (connection.getDestination().equals(this)) {
+                    // don't add the connection twice. This may occur if we have a self-loop because we will be told
+                    // to add the connection once because we are the source and again because we are the destination.
+                    if (!incomingConnections.contains(connection)) {
+                        incomingConnections.add(connection);
+                    }
+
+                    return;
+                } else {
+                    throw new IllegalArgumentException("Cannot add a connection to a LocalPort for which the LocalPort is neither the Source nor the Destination");
+                }
+            }
+
+            /* TODO: Will commenting this out have repercussions?
+            Needed to comment this out to allow use of relationships for port to processor case which was previously not supported
+            for (final Relationship relationship : connection.getRelationships()) {
+                if (!relationship.equals(PORT_RELATIONSHIP)) {
+                    throw new IllegalArgumentException("No relationship with name " + relationship + " exists for Local Ports");
+                }
+            }
+            */
+
+            // don't add the connection twice. This may occur if we have a self-loop because we will be told
+            // to add the connection once because we are the source and again because we are the destination.
+            if (!outgoingConnections.contains(connection)) {
+                outgoingConnections.add(connection);
+            }
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    public boolean hasIncomingConnection() {
+        readLock.lock();
+        try {
+            return !incomingConnections.isEmpty();
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSessionFactory sessionFactory) throws ProcessException {
+        final ProcessSession session = sessionFactory.createSession();
+
+        try {
+            onTrigger(context, session);
+            session.commit();
+        } catch (final ProcessException e) {
+            session.rollback();
+            throw e;
+        } catch (final Throwable t) {
+            session.rollback();
+            throw new RuntimeException(t);
+        }
+    }
+
+    public abstract void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException;
+
+    @Override
+    public void updateConnection(final Connection connection) throws IllegalStateException {
+        if (requireNonNull(connection).getSource().equals(this)) {
+            writeLock.lock();
+            try {
+                if (!outgoingConnections.remove(connection)) {
+                    throw new IllegalStateException("No Connection with ID " + connection.getIdentifier() + " is currently registered with this Port");
+                }
+                outgoingConnections.add(connection);
+            } finally {
+                writeLock.unlock();
+            }
+        } else if (connection.getDestination().equals(this)) {
+            writeLock.lock();
+            try {
+                if (!incomingConnections.remove(connection)) {
+                    throw new IllegalStateException("No Connection with ID " + connection.getIdentifier() + " is currently registered with this Port");
+                }
+                incomingConnections.add(connection);
+            } finally {
+                writeLock.unlock();
+            }
+        } else {
+            throw new IllegalStateException("The given connection is not currently registered for this Port");
+        }
+    }
+
+    @Override
+    public void removeConnection(final Connection connection) throws IllegalArgumentException, IllegalStateException {
+        writeLock.lock();
+        try {
+            if (!requireNonNull(connection).getSource().equals(this)) {
+                final boolean existed = incomingConnections.remove(connection);
+                if (!existed) {
+                    throw new IllegalStateException("The given connection is not currently registered for this Port");
+                }
+                return;
+            }
+
+            if (!canConnectionBeRemoved(connection)) {
+                // TODO: Determine which processors will be broken if connection is removed, rather than just returning a boolean
+                throw new IllegalStateException("Connection " + connection.getIdentifier() + " cannot be removed");
+            }
+
+            final boolean removed = outgoingConnections.remove(connection);
+            if (!removed) {
+                throw new IllegalStateException("Connection " + connection.getIdentifier() + " is not registered with " + this.getIdentifier());
+            }
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    /**
+     * Verify that removing this connection will not prevent this Port from
+     * still being connected via each relationship
+     *
+     * @param connection to test for removal
+     * @return true if can be removed
+     */
+    private boolean canConnectionBeRemoved(final Connection connection) {
+        final Connectable source = connection.getSource();
+        if (!source.isRunning()) {
+            // we don't have to verify that this Connectable is still connected because it's okay to make
+            // the source invalid since it is not running.
+            return true;
+        }
+
+        for (final Relationship relationship : source.getRelationships()) {
+            if (source.isAutoTerminated(relationship)) {
+                continue;
+            }
+
+            final Set<Connection> connectionsForRelationship = source.getConnections(relationship);
+            if (connectionsForRelationship == null || connectionsForRelationship.isEmpty()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public Set<Connection> getConnections() {
+        readLock.lock();
+        try {
+            return Collections.unmodifiableSet(outgoingConnections);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public Set<Connection> getConnections(final Relationship relationship) {
+        readLock.lock();
+        try {
+            if (relationship.equals(PORT_RELATIONSHIP)) {
+                return Collections.unmodifiableSet(outgoingConnections);
+            }
+
+            throw new IllegalArgumentException("No relationship with name " + relationship.getName() + " exists for Local Ports");
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public Position getPosition() {
+        return position.get();
+    }
+
+    @Override
+    public void setPosition(final Position position) {
+        this.position.set(position);
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("id", getIdentifier()).toString();
+    }
+
+    @Override
+    public List<Connection> getIncomingConnections() {
+        readLock.lock();
+        try {
+            return Collections.unmodifiableList(incomingConnections);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public abstract boolean isValid();
+
+    @Override
+    public boolean isAutoTerminated(final Relationship relationship) {
+        return false;
+    }
+
+    @Override
+    public boolean isLossTolerant() {
+        return lossTolerant.get();
+    }
+
+    @Override
+    public void setLossTolerant(boolean lossTolerant) {
+        this.lossTolerant.set(lossTolerant);
+    }
+
+    @Override
+    public void setMaxConcurrentTasks(final int taskCount) {
+        if (taskCount < 1) {
+            throw new IllegalArgumentException();
+        }
+        concurrentTaskCount.set(taskCount);
+    }
+
+    @Override
+    public int getMaxConcurrentTasks() {
+        return concurrentTaskCount.get();
+    }
+
+    @Override
+    public void shutdown() {
+        scheduledState.set(ScheduledState.STOPPED);
+    }
+
+    @Override
+    public void onSchedulingStart() {
+        scheduledState.set(ScheduledState.RUNNING);
+    }
+
+    public void disable() {
+        final boolean updated = scheduledState.compareAndSet(ScheduledState.STOPPED, ScheduledState.DISABLED);
+        if (!updated) {
+            throw new IllegalStateException("Port cannot be disabled because it is not stopped");
+        }
+    }
+
+    public void enable() {
+        final boolean updated = scheduledState.compareAndSet(ScheduledState.DISABLED, ScheduledState.STOPPED);
+        if (!updated) {
+            throw new IllegalStateException("Port cannot be enabled because it is not disabled");
+        }
+    }
+
+    @Override
+    public boolean isRunning() {
+        return getScheduledState().equals(ScheduledState.RUNNING) || processScheduler.getActiveThreadCount(this) > 0;
+    }
+
+    @Override
+    public ScheduledState getScheduledState() {
+        return scheduledState.get();
+    }
+
+    @Override
+    public ConnectableType getConnectableType() {
+        return type;
+    }
+
+    @Override
+    public void setYieldPeriod(final String yieldPeriod) {
+        final long yieldMillis = FormatUtils.getTimeDuration(requireNonNull(yieldPeriod), TimeUnit.MILLISECONDS);
+        if (yieldMillis < 0) {
+            throw new IllegalArgumentException("Yield duration must be positive");
+        }
+        this.yieldPeriod.set(yieldPeriod);
+    }
+
+    @Override
+    public void setScheduldingPeriod(final String schedulingPeriod) {
+        final long schedulingNanos = FormatUtils.getTimeDuration(requireNonNull(schedulingPeriod), TimeUnit.NANOSECONDS);
+        if (schedulingNanos < 0) {
+            throw new IllegalArgumentException("Scheduling Period must be positive");
+        }
+
+        this.schedulingPeriod.set(schedulingPeriod);
+        this.schedulingNanos.set(Math.max(MINIMUM_SCHEDULING_NANOS, schedulingNanos));
+    }
+
+    @Override
+    public long getPenalizationPeriod(final TimeUnit timeUnit) {
+        return FormatUtils.getTimeDuration(getPenalizationPeriod(), timeUnit == null ? DEFAULT_TIME_UNIT : timeUnit);
+    }
+
+    @Override
+    public String getPenalizationPeriod() {
+        return penalizationPeriod.get();
+    }
+
+    @Override
+    public void yield() {
+        final long yieldMillis = getYieldPeriod(TimeUnit.MILLISECONDS);
+        yield(yieldMillis, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void yield(final long yieldDuration, final TimeUnit timeUnit) {
+        final long yieldMillis = timeUnit.toMillis(yieldDuration);
+        yieldExpiration.set(Math.max(yieldExpiration.get(), System.currentTimeMillis() + yieldMillis));
+    }
+
+    @Override
+    public long getYieldExpiration() {
+        return yieldExpiration.get();
+    }
+
+    @Override
+    public long getSchedulingPeriod(final TimeUnit timeUnit) {
+        return timeUnit.convert(schedulingNanos.get(), TimeUnit.NANOSECONDS);
+    }
+
+    @Override
+    public String getSchedulingPeriod() {
+        return schedulingPeriod.get();
+    }
+
+    @Override
+    public void setPenalizationPeriod(final String penalizationPeriod) {
+        this.penalizationPeriod.set(penalizationPeriod);
+    }
+
+    @Override
+    public String getYieldPeriod() {
+        return yieldPeriod.get();
+    }
+
+    @Override
+    public long getYieldPeriod(final TimeUnit timeUnit) {
+        return FormatUtils.getTimeDuration(getYieldPeriod(), timeUnit == null ? DEFAULT_TIME_UNIT : timeUnit);
+    }
+
+    @Override
+    public void verifyCanDelete() throws IllegalStateException {
+        verifyCanDelete(false);
+    }
+
+    @Override
+    public void verifyCanDelete(final boolean ignoreConnections) {
+        readLock.lock();
+        try {
+            if (isRunning()) {
+                throw new IllegalStateException(this.getIdentifier() + " is running");
+            }
+
+            if (!ignoreConnections) {
+                for (final Connection connection : outgoingConnections) {
+                    connection.verifyCanDelete();
+                }
+
+                for (final Connection connection : incomingConnections) {
+                    if (connection.getSource().equals(this)) {
+                        connection.verifyCanDelete();
+                    } else {
+                        throw new IllegalStateException(this.getIdentifier() + " is the destination of another component");
+                    }
+                }
+            }
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void verifyCanStart() {
+        readLock.lock();
+        try {
+            switch (scheduledState.get()) {
+                case DISABLED:
+                    throw new IllegalStateException(this.getIdentifier() + " cannot be started because it is disabled");
+                case RUNNING:
+                    throw new IllegalStateException(this.getIdentifier() + " cannot be started because it is already running");
+                case STOPPED:
+                    break;
+            }
+            verifyNoActiveThreads();
+
+            final Collection<ValidationResult> validationResults = getValidationErrors();
+            if (!validationResults.isEmpty()) {
+                throw new IllegalStateException(this.getIdentifier() + " is not in a valid state: " + validationResults.iterator().next().getExplanation());
+            }
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void verifyCanStop() {
+        if (getScheduledState() != ScheduledState.RUNNING) {
+            throw new IllegalStateException(this.getIdentifier() + " is not scheduled to run");
+        }
+    }
+
+    @Override
+    public void verifyCanUpdate() {
+        readLock.lock();
+        try {
+            if (isRunning()) {
+                throw new IllegalStateException(this.getIdentifier() + " is not stopped");
+            }
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void verifyCanEnable() {
+        readLock.lock();
+        try {
+            if (getScheduledState() != ScheduledState.DISABLED) {
+                throw new IllegalStateException(this.getIdentifier() + " is not disabled");
+            }
+
+            verifyNoActiveThreads();
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void verifyCanDisable() {
+        readLock.lock();
+        try {
+            if (getScheduledState() != ScheduledState.STOPPED) {
+                throw new IllegalStateException(this.getIdentifier() + " is not stopped");
+            }
+            verifyNoActiveThreads();
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    private void verifyNoActiveThreads() throws IllegalStateException {
+        final int threadCount = processScheduler.getActiveThreadCount(this);
+        if (threadCount > 0) {
+            throw new IllegalStateException(this.getIdentifier() + " has " + threadCount + " threads still active");
+        }
+    }
+
+    @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");
+            }
+        }
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java
new file mode 100644 (file)
index 0000000..ec15ba6
--- /dev/null
@@ -0,0 +1,105 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed 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.
+ * ============LICENSE_END=========================================================
+ */
+package org.apache.nifi.nar;
+
+import org.apache.nifi.bundle.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executors;
+
+/**
+ * Uses the Java executor service scheduler to continuously load new DCAE jars
+ */
+public class DCAEAutoLoader {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(DCAEAutoLoader.class);
+
+    private static final long POLL_INTERVAL_MS = 5000;
+
+    /**
+     * Runnable task that grabs list of remotely stored jars, identifies ones that haven't
+     * been processed, builds Nifi bundles for those unprocessed ones and loads them into
+     * the global extension manager.
+     */
+    private static class LoaderTask implements Runnable {
+
+        private static final Logger LOGGER = LoggerFactory.getLogger(LoaderTask.class);
+
+        private final URI indexJsonDcaeJars;
+        private final ExtensionDiscoveringManager extensionManager;
+        private final Set<URL> processed = new LinkedHashSet();
+
+        private LoaderTask(URI indexJsonDcaeJars, ExtensionDiscoveringManager extensionManager) {
+            this.indexJsonDcaeJars = indexJsonDcaeJars;
+            this.extensionManager = extensionManager;
+        }
+
+        @Override
+        public void run() {
+            try {
+                List<URL> toProcess = DCAEClassLoaders.getDCAEJarsURLs(this.indexJsonDcaeJars);
+                toProcess.removeAll(processed);
+
+                if (!toProcess.isEmpty()) {
+                    Set<Bundle> bundles = DCAEClassLoaders.createDCAEBundles(toProcess);
+                    this.extensionManager.discoverExtensions(bundles);
+                    processed.addAll(toProcess);
+
+                    LOGGER.info(String.format("#Added DCAE bundles: %d, #Total DCAE bundles: %d ",
+                        bundles.size(), processed.size()));
+                }
+            } catch (final Exception e) {
+                LOGGER.error("Error loading DCAE jars due to: " + e.getMessage(), e);
+            }
+        }
+    }
+
+    private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+    private ScheduledFuture taskFuture;
+
+    public synchronized void start(URI indexJsonDcaeJars, final ExtensionDiscoveringManager extensionManager) {
+        // Restricting to a single thread
+        if (taskFuture != null && !taskFuture.isCancelled()) {
+            return;
+        }
+
+        LOGGER.info("Starting DCAE Auto-Loader: {}", new Object[]{indexJsonDcaeJars});
+
+        LoaderTask task = new LoaderTask(indexJsonDcaeJars, extensionManager);
+        this.taskFuture = executor.scheduleAtFixedRate(task, 0, POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);
+        LOGGER.info("DCAE Auto-Loader started");
+    }
+
+    public synchronized void stop() {
+        if (this.taskFuture != null) {
+            this.taskFuture.cancel(true);
+            LOGGER.info("DCAE Auto-Loader stopped");
+        }
+    }
+
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java
new file mode 100644 (file)
index 0000000..a4dbe77
--- /dev/null
@@ -0,0 +1,127 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed 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.
+ * ============LICENSE_END=========================================================
+ */
+package org.apache.nifi.nar;
+
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.bundle.BundleDetails;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.jar.Attributes;
+
+
+/**
+ * Class responsible for loading JARs for DCAEProcessors into Nifi
+ */
+public class DCAEClassLoaders {
+
+    public static class DCAEClassLoadersError extends RuntimeException {
+        public DCAEClassLoadersError(Throwable e) {
+            super("Error while using DCAEClassLoaders", e);
+        }
+    }
+
+    /**
+     * Given a URL to a index.json file, fetches the file and generates a list of
+     * URLs for DCAE jars that has Processors packaged.
+     * 
+     * @param indexDCAEJars
+     * @return
+     */
+    public static List<URL> getDCAEJarsURLs(URI indexDCAEJars) {
+        JsonFactory jf = new JsonFactory();
+        ObjectMapper om = new ObjectMapper();
+
+        try {
+            List<Object> urls = om.readValue(jf.createParser(indexDCAEJars.toURL()), List.class);
+
+            return urls.stream().map(u -> {
+                try {
+                    Map<String, Object> foo = (Map<String, Object>) u;
+                    String name = (String) foo.get("name");
+                    String url = String.format("%s/%s", indexDCAEJars.toString(), name);
+                    return new URL(url);
+                } catch (MalformedURLException e) {
+                    // Hopefully you never come here...
+                    return null;
+                }
+            }).collect(Collectors.toList());
+        } catch (Exception e) {
+            throw new RuntimeException("Error while getting jar URIs", e);
+        }
+    }
+
+    private static BundleDetails createBundleDetails(URLClassLoader classLoader) {
+        try {
+            URL url = classLoader.findResource("META-INF/MANIFEST.MF");
+            Manifest manifest = new Manifest(url.openStream());
+
+            final Attributes attributes = manifest.getMainAttributes();
+
+            final BundleDetails.Builder builder = new BundleDetails.Builder();
+            // NOTE: Working directory cannot be null so set it to some bogus dir
+            // because we aren't really using this. Or maybe should create our own
+            // working directory
+            builder.workingDir(new File("/tmp"));
+
+            final String group = attributes.getValue("Group");
+            final String id = attributes.getValue("Id");
+            final String version = attributes.getValue("Version");
+            builder.coordinate(new BundleCoordinate(group, id, version));
+
+            return builder.build();
+        } catch (IOException e) {
+            throw new DCAEClassLoadersError(e);
+        }
+    }
+
+    /**
+     * From a list of URLs to remote JARs where the JARs contain DCAEProcessor classes,
+     * create a bundle for each JAR. You will never get a partial list of bundles.
+     * 
+     * @param jarURLs
+     * @return
+     */
+    public static Set<Bundle> createDCAEBundles(List<URL> jarURLs) {
+        Set<Bundle> bundles = new HashSet<>();
+
+        for (URL jarURL : jarURLs) {
+            URLClassLoader classLoader = new URLClassLoader(new URL[] {jarURL});
+            Bundle bundle = new Bundle(createBundleDetails(classLoader), classLoader);
+            bundles.add(bundle);
+        }
+
+        return bundles;
+     }
+
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java
new file mode 100644 (file)
index 0000000..3b341ec
--- /dev/null
@@ -0,0 +1,1551 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.util;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * The NiFiProperties class holds all properties which are needed for various
+ * values to be available at runtime. It is strongly tied to the startup
+ * properties needed and is often refer to as the 'nifi.properties' file. The
+ * properties contains keys and values. Great care should be taken in leveraging
+ * this class or passing it along. Its use should be refactored and minimized
+ * over time.
+ */
+public abstract class NiFiProperties {
+
+    // core properties
+    public static final String PROPERTIES_FILE_PATH = "nifi.properties.file.path";
+    public static final String FLOW_CONFIGURATION_FILE = "nifi.flow.configuration.file";
+    public static final String FLOW_CONFIGURATION_ARCHIVE_ENABLED = "nifi.flow.configuration.archive.enabled";
+    public static final String FLOW_CONFIGURATION_ARCHIVE_DIR = "nifi.flow.configuration.archive.dir";
+    public static final String FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = "nifi.flow.configuration.archive.max.time";
+    public static final String FLOW_CONFIGURATION_ARCHIVE_MAX_STORAGE = "nifi.flow.configuration.archive.max.storage";
+    public static final String FLOW_CONFIGURATION_ARCHIVE_MAX_COUNT = "nifi.flow.configuration.archive.max.count";
+    public static final String AUTHORIZER_CONFIGURATION_FILE = "nifi.authorizer.configuration.file";
+    public static final String LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE = "nifi.login.identity.provider.configuration.file";
+    public static final String REPOSITORY_DATABASE_DIRECTORY = "nifi.database.directory";
+    public static final String RESTORE_DIRECTORY = "nifi.restore.directory";
+    public static final String WRITE_DELAY_INTERVAL = "nifi.flowservice.writedelay.interval";
+    public static final String AUTO_RESUME_STATE = "nifi.flowcontroller.autoResumeState";
+    public static final String FLOW_CONTROLLER_GRACEFUL_SHUTDOWN_PERIOD = "nifi.flowcontroller.graceful.shutdown.period";
+    public static final String NAR_LIBRARY_DIRECTORY = "nifi.nar.library.directory";
+    public static final String NAR_LIBRARY_DIRECTORY_PREFIX = "nifi.nar.library.directory.";
+    public static final String NAR_LIBRARY_AUTOLOAD_DIRECTORY = "nifi.nar.library.autoload.directory";
+    public static final String NAR_WORKING_DIRECTORY = "nifi.nar.working.directory";
+    public static final String COMPONENT_DOCS_DIRECTORY = "nifi.documentation.working.directory";
+    public static final String SENSITIVE_PROPS_KEY = "nifi.sensitive.props.key";
+    public static final String SENSITIVE_PROPS_ALGORITHM = "nifi.sensitive.props.algorithm";
+    public static final String SENSITIVE_PROPS_PROVIDER = "nifi.sensitive.props.provider";
+    public static final String H2_URL_APPEND = "nifi.h2.url.append";
+    public static final String REMOTE_INPUT_HOST = "nifi.remote.input.host";
+    public static final String REMOTE_INPUT_PORT = "nifi.remote.input.socket.port";
+    public static final String SITE_TO_SITE_SECURE = "nifi.remote.input.secure";
+    public static final String SITE_TO_SITE_HTTP_ENABLED = "nifi.remote.input.http.enabled";
+    public static final String SITE_TO_SITE_HTTP_TRANSACTION_TTL = "nifi.remote.input.http.transaction.ttl";
+    public static final String REMOTE_CONTENTS_CACHE_EXPIRATION = "nifi.remote.contents.cache.expiration";
+    public static final String TEMPLATE_DIRECTORY = "nifi.templates.directory";
+    public static final String ADMINISTRATIVE_YIELD_DURATION = "nifi.administrative.yield.duration";
+    public static final String PERSISTENT_STATE_DIRECTORY = "nifi.persistent.state.directory";
+    public static final String BORED_YIELD_DURATION = "nifi.bored.yield.duration";
+    public static final String PROCESSOR_SCHEDULING_TIMEOUT = "nifi.processor.scheduling.timeout";
+    public static final String BACKPRESSURE_COUNT = "nifi.queue.backpressure.count";
+    public static final String BACKPRESSURE_SIZE = "nifi.queue.backpressure.size";
+
+    // DCAE related config
+    public static final String DCAE_JARS_INDEX_URL = "nifi.dcae.jars.index.url";
+
+    // content repository properties
+    public static final String REPOSITORY_CONTENT_PREFIX = "nifi.content.repository.directory.";
+    public static final String CONTENT_REPOSITORY_IMPLEMENTATION = "nifi.content.repository.implementation";
+    public static final String MAX_APPENDABLE_CLAIM_SIZE = "nifi.content.claim.max.appendable.size";
+    public static final String MAX_FLOWFILES_PER_CLAIM = "nifi.content.claim.max.flow.files";
+    public static final String CONTENT_ARCHIVE_MAX_RETENTION_PERIOD = "nifi.content.repository.archive.max.retention.period";
+    public static final String CONTENT_ARCHIVE_MAX_USAGE_PERCENTAGE = "nifi.content.repository.archive.max.usage.percentage";
+    public static final String CONTENT_ARCHIVE_BACK_PRESSURE_PERCENTAGE = "nifi.content.repository.archive.backpressure.percentage";
+    public static final String CONTENT_ARCHIVE_ENABLED = "nifi.content.repository.archive.enabled";
+    public static final String CONTENT_ARCHIVE_CLEANUP_FREQUENCY = "nifi.content.repository.archive.cleanup.frequency";
+    public static final String CONTENT_VIEWER_URL = "nifi.content.viewer.url";
+
+    // flowfile repository properties
+    public static final String FLOWFILE_REPOSITORY_IMPLEMENTATION = "nifi.flowfile.repository.implementation";
+    public static final String FLOWFILE_REPOSITORY_ALWAYS_SYNC = "nifi.flowfile.repository.always.sync";
+    public static final String FLOWFILE_REPOSITORY_DIRECTORY = "nifi.flowfile.repository.directory";
+    public static final String FLOWFILE_REPOSITORY_PARTITIONS = "nifi.flowfile.repository.partitions";
+    public static final String FLOWFILE_REPOSITORY_CHECKPOINT_INTERVAL = "nifi.flowfile.repository.checkpoint.interval";
+    public static final String FLOWFILE_SWAP_MANAGER_IMPLEMENTATION = "nifi.swap.manager.implementation";
+    public static final String QUEUE_SWAP_THRESHOLD = "nifi.queue.swap.threshold";
+    public static final String SWAP_IN_THREADS = "nifi.swap.in.threads";
+    public static final String SWAP_IN_PERIOD = "nifi.swap.in.period";
+    public static final String SWAP_OUT_THREADS = "nifi.swap.out.threads";
+    public static final String SWAP_OUT_PERIOD = "nifi.swap.out.period";
+
+    // provenance properties
+    public static final String PROVENANCE_REPO_IMPLEMENTATION_CLASS = "nifi.provenance.repository.implementation";
+    public static final String PROVENANCE_REPO_DIRECTORY_PREFIX = "nifi.provenance.repository.directory.";
+    public static final String PROVENANCE_MAX_STORAGE_TIME = "nifi.provenance.repository.max.storage.time";
+    public static final String PROVENANCE_MAX_STORAGE_SIZE = "nifi.provenance.repository.max.storage.size";
+    public static final String PROVENANCE_ROLLOVER_TIME = "nifi.provenance.repository.rollover.time";
+    public static final String PROVENANCE_ROLLOVER_SIZE = "nifi.provenance.repository.rollover.size";
+    public static final String PROVENANCE_QUERY_THREAD_POOL_SIZE = "nifi.provenance.repository.query.threads";
+    public static final String PROVENANCE_INDEX_THREAD_POOL_SIZE = "nifi.provenance.repository.index.threads";
+    public static final String PROVENANCE_COMPRESS_ON_ROLLOVER = "nifi.provenance.repository.compress.on.rollover";
+    public static final String PROVENANCE_INDEXED_FIELDS = "nifi.provenance.repository.indexed.fields";
+    public static final String PROVENANCE_INDEXED_ATTRIBUTES = "nifi.provenance.repository.indexed.attributes";
+    public static final String PROVENANCE_INDEX_SHARD_SIZE = "nifi.provenance.repository.index.shard.size";
+    public static final String PROVENANCE_JOURNAL_COUNT = "nifi.provenance.repository.journal.count";
+    public static final String PROVENANCE_REPO_ENCRYPTION_KEY = "nifi.provenance.repository.encryption.key";
+    public static final String PROVENANCE_REPO_ENCRYPTION_KEY_ID = "nifi.provenance.repository.encryption.key.id";
+    public static final String PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS = "nifi.provenance.repository.encryption.key.provider.implementation";
+    public static final String PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_LOCATION = "nifi.provenance.repository.encryption.key.provider.location";
+    public static final String PROVENANCE_REPO_DEBUG_FREQUENCY = "nifi.provenance.repository.debug.frequency";
+
+    // component status repository properties
+    public static final String COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION = "nifi.components.status.repository.implementation";
+    public static final String COMPONENT_STATUS_SNAPSHOT_FREQUENCY = "nifi.components.status.snapshot.frequency";
+
+    // security properties
+    public static final String SECURITY_KEYSTORE = "nifi.security.keystore";
+    public static final String SECURITY_KEYSTORE_TYPE = "nifi.security.keystoreType";
+    public static final String SECURITY_KEYSTORE_PASSWD = "nifi.security.keystorePasswd";
+    public static final String SECURITY_KEY_PASSWD = "nifi.security.keyPasswd";
+    public static final String SECURITY_TRUSTSTORE = "nifi.security.truststore";
+    public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.security.truststoreType";
+    public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.security.truststorePasswd";
+    public static final String SECURITY_USER_AUTHORIZER = "nifi.security.user.authorizer";
+    public static final String SECURITY_USER_LOGIN_IDENTITY_PROVIDER = "nifi.security.user.login.identity.provider";
+    public static final String SECURITY_OCSP_RESPONDER_URL = "nifi.security.ocsp.responder.url";
+    public static final String SECURITY_OCSP_RESPONDER_CERTIFICATE = "nifi.security.ocsp.responder.certificate";
+    public static final String SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX = "nifi.security.identity.mapping.pattern.";
+    public static final String SECURITY_IDENTITY_MAPPING_VALUE_PREFIX = "nifi.security.identity.mapping.value.";
+    public static final String SECURITY_IDENTITY_MAPPING_TRANSFORM_PREFIX = "nifi.security.identity.mapping.transform.";
+    public static final String SECURITY_GROUP_MAPPING_PATTERN_PREFIX = "nifi.security.group.mapping.pattern.";
+    public static final String SECURITY_GROUP_MAPPING_VALUE_PREFIX = "nifi.security.group.mapping.value.";
+    public static final String SECURITY_GROUP_MAPPING_TRANSFORM_PREFIX = "nifi.security.group.mapping.transform.";
+
+    // oidc
+    public static final String SECURITY_USER_OIDC_DISCOVERY_URL = "nifi.security.user.oidc.discovery.url";
+    public static final String SECURITY_USER_OIDC_CONNECT_TIMEOUT = "nifi.security.user.oidc.connect.timeout";
+    public static final String SECURITY_USER_OIDC_READ_TIMEOUT = "nifi.security.user.oidc.read.timeout";
+    public static final String SECURITY_USER_OIDC_CLIENT_ID = "nifi.security.user.oidc.client.id";
+    public static final String SECURITY_USER_OIDC_CLIENT_SECRET = "nifi.security.user.oidc.client.secret";
+    public static final String SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM = "nifi.security.user.oidc.preferred.jwsalgorithm";
+
+    // apache knox
+    public static final String SECURITY_USER_KNOX_URL = "nifi.security.user.knox.url";
+    public static final String SECURITY_USER_KNOX_PUBLIC_KEY = "nifi.security.user.knox.publicKey";
+    public static final String SECURITY_USER_KNOX_COOKIE_NAME = "nifi.security.user.knox.cookieName";
+    public static final String SECURITY_USER_KNOX_AUDIENCES = "nifi.security.user.knox.audiences";
+
+    // web properties
+    public static final String WEB_WAR_DIR = "nifi.web.war.directory";
+    public static final String WEB_HTTP_PORT = "nifi.web.http.port";
+    public static final String WEB_HTTP_PORT_FORWARDING = "nifi.web.http.port.forwarding";
+    public static final String WEB_HTTP_HOST = "nifi.web.http.host";
+    public static final String WEB_HTTP_NETWORK_INTERFACE_PREFIX = "nifi.web.http.network.interface.";
+    public static final String WEB_HTTPS_PORT = "nifi.web.https.port";
+    public static final String WEB_HTTPS_PORT_FORWARDING = "nifi.web.https.port.forwarding";
+    public static final String WEB_HTTPS_HOST = "nifi.web.https.host";
+    public static final String WEB_HTTPS_NETWORK_INTERFACE_PREFIX = "nifi.web.https.network.interface.";
+    public static final String WEB_WORKING_DIR = "nifi.web.jetty.working.directory";
+    public static final String WEB_THREADS = "nifi.web.jetty.threads";
+    public static final String WEB_MAX_HEADER_SIZE = "nifi.web.max.header.size";
+    public static final String WEB_PROXY_CONTEXT_PATH = "nifi.web.proxy.context.path";
+    public static final String WEB_PROXY_HOST = "nifi.web.proxy.host";
+
+    // ui properties
+    public static final String UI_BANNER_TEXT = "nifi.ui.banner.text";
+    public static final String UI_AUTO_REFRESH_INTERVAL = "nifi.ui.autorefresh.interval";
+    public static  final String UI_DCAE_DISTRIBUTOR_API_URL="nifi.ui.dcae.distibutor.api.url";
+
+    // cluster common properties
+    public static final String CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL = "nifi.cluster.protocol.heartbeat.interval";
+    public static final String CLUSTER_PROTOCOL_IS_SECURE = "nifi.cluster.protocol.is.secure";
+
+    // cluster node properties
+    public static final String CLUSTER_IS_NODE = "nifi.cluster.is.node";
+    public static final String CLUSTER_NODE_ADDRESS = "nifi.cluster.node.address";
+    public static final String CLUSTER_NODE_PROTOCOL_PORT = "nifi.cluster.node.protocol.port";
+    public static final String CLUSTER_NODE_PROTOCOL_THREADS = "nifi.cluster.node.protocol.threads";
+    public static final String CLUSTER_NODE_PROTOCOL_MAX_THREADS = "nifi.cluster.node.protocol.max.threads";
+    public static final String CLUSTER_NODE_CONNECTION_TIMEOUT = "nifi.cluster.node.connection.timeout";
+    public static final String CLUSTER_NODE_READ_TIMEOUT = "nifi.cluster.node.read.timeout";
+    public static final String CLUSTER_NODE_MAX_CONCURRENT_REQUESTS = "nifi.cluster.node.max.concurrent.requests";
+    public static final String CLUSTER_FIREWALL_FILE = "nifi.cluster.firewall.file";
+    public static final String FLOW_ELECTION_MAX_WAIT_TIME = "nifi.cluster.flow.election.max.wait.time";
+    public static final String FLOW_ELECTION_MAX_CANDIDATES = "nifi.cluster.flow.election.max.candidates";
+
+    // cluster load balance properties
+    public static final String LOAD_BALANCE_ADDRESS = "nifi.cluster.load.balance.address";
+    public static final String LOAD_BALANCE_PORT = "nifi.cluster.load.balance.port";
+    public static final String LOAD_BALANCE_CONNECTIONS_PER_NODE = "nifi.cluster.load.balance.connections.per.node";
+    public static final String LOAD_BALANCE_MAX_THREAD_COUNT = "nifi.cluster.load.balance.max.thread.count";
+    public static final String LOAD_BALANCE_COMMS_TIMEOUT = "nifi.cluster.load.balance.comms.timeout";
+
+    // zookeeper properties
+    public static final String ZOOKEEPER_CONNECT_STRING = "nifi.zookeeper.connect.string";
+    public static final String ZOOKEEPER_CONNECT_TIMEOUT = "nifi.zookeeper.connect.timeout";
+    public static final String ZOOKEEPER_SESSION_TIMEOUT = "nifi.zookeeper.session.timeout";
+    public static final String ZOOKEEPER_ROOT_NODE = "nifi.zookeeper.root.node";
+    public static final String ZOOKEEPER_AUTH_TYPE = "nifi.zookeeper.auth.type";
+    public static final String ZOOKEEPER_KERBEROS_REMOVE_HOST_FROM_PRINCIPAL = "nifi.zookeeper.kerberos.removeHostFromPrincipal";
+    public static final String ZOOKEEPER_KERBEROS_REMOVE_REALM_FROM_PRINCIPAL = "nifi.zookeeper.kerberos.removeRealmFromPrincipal";
+
+    // kerberos properties
+    public static final String KERBEROS_KRB5_FILE = "nifi.kerberos.krb5.file";
+    public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.kerberos.service.principal";
+    public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.kerberos.service.keytab.location";
+    public static final String KERBEROS_SPNEGO_PRINCIPAL = "nifi.kerberos.spnego.principal";
+    public static final String KERBEROS_SPNEGO_KEYTAB_LOCATION = "nifi.kerberos.spnego.keytab.location";
+    public static final String KERBEROS_AUTHENTICATION_EXPIRATION = "nifi.kerberos.spnego.authentication.expiration";
+
+    // state management
+    public static final String STATE_MANAGEMENT_CONFIG_FILE = "nifi.state.management.configuration.file";
+    public static final String STATE_MANAGEMENT_LOCAL_PROVIDER_ID = "nifi.state.management.provider.local";
+    public static final String STATE_MANAGEMENT_CLUSTER_PROVIDER_ID = "nifi.state.management.provider.cluster";
+    public static final String STATE_MANAGEMENT_START_EMBEDDED_ZOOKEEPER = "nifi.state.management.embedded.zookeeper.start";
+    public static final String STATE_MANAGEMENT_ZOOKEEPER_PROPERTIES = "nifi.state.management.embedded.zookeeper.properties";
+
+    // expression language properties
+    public static final String VARIABLE_REGISTRY_PROPERTIES = "nifi.variable.registry.properties";
+
+    // defaults
+    public static final Boolean DEFAULT_AUTO_RESUME_STATE = true;
+    public static final String DEFAULT_AUTHORIZER_CONFIGURATION_FILE = "conf/authorizers.xml";
+    public static final String DEFAULT_LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE = "conf/login-identity-providers.xml";
+    public static final Integer DEFAULT_REMOTE_INPUT_PORT = null;
+    public static final Path DEFAULT_TEMPLATE_DIRECTORY = Paths.get("conf", "templates");
+    public static final int DEFAULT_WEB_THREADS = 200;
+    public static final String DEFAULT_WEB_MAX_HEADER_SIZE = "16 KB";
+    public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
+    public static final String DEFAULT_NAR_WORKING_DIR = "./work/nar";
+    public static final String DEFAULT_COMPONENT_DOCS_DIRECTORY = "./work/docs/components";
+    public static final String DEFAULT_NAR_LIBRARY_DIR = "./lib";
+    public static final String DEFAULT_NAR_LIBRARY_AUTOLOAD_DIR = "./extensions";
+    public static final String DEFAULT_FLOWFILE_REPO_PARTITIONS = "256";
+    public static final String DEFAULT_FLOWFILE_CHECKPOINT_INTERVAL = "2 min";
+    public static final int DEFAULT_MAX_FLOWFILES_PER_CLAIM = 100;
+    public static final String DEFAULT_MAX_APPENDABLE_CLAIM_SIZE = "1 MB";
+    public static final int DEFAULT_QUEUE_SWAP_THRESHOLD = 20000;
+    public static final String DEFAULT_SWAP_STORAGE_LOCATION = "./flowfile_repository/swap";
+    public static final String DEFAULT_SWAP_IN_PERIOD = "1 sec";
+    public static final String DEFAULT_SWAP_OUT_PERIOD = "5 sec";
+    public static final int DEFAULT_SWAP_IN_THREADS = 4;
+    public static final int DEFAULT_SWAP_OUT_THREADS = 4;
+    public static final long DEFAULT_BACKPRESSURE_COUNT = 10_000L;
+    public static final String DEFAULT_BACKPRESSURE_SIZE = "1 GB";
+    public static final String DEFAULT_ADMINISTRATIVE_YIELD_DURATION = "30 sec";
+    public static final String DEFAULT_PERSISTENT_STATE_DIRECTORY = "./conf/state";
+    public static final String DEFAULT_COMPONENT_STATUS_SNAPSHOT_FREQUENCY = "5 mins";
+    public static final String DEFAULT_BORED_YIELD_DURATION = "10 millis";
+    public static final String DEFAULT_ZOOKEEPER_CONNECT_TIMEOUT = "3 secs";
+    public static final String DEFAULT_ZOOKEEPER_SESSION_TIMEOUT = "3 secs";
+    public static final String DEFAULT_ZOOKEEPER_ROOT_NODE = "/nifi";
+    public static final String DEFAULT_ZOOKEEPER_AUTH_TYPE = "default";
+    public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_HOST_FROM_PRINCIPAL = "true";
+    public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_REALM_FROM_PRINCIPAL = "true";
+    public static final String DEFAULT_SITE_TO_SITE_HTTP_TRANSACTION_TTL = "30 secs";
+    public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED = "true";
+    public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = "30 days";
+    public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_STORAGE = "500 MB";
+    public static final String DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT = "5 secs";
+    public static final String DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT = "5 secs";
+
+    // DCAE related config
+    // REVIEW: Default is to turn off the dcae jar loading until the platform becomes more accessible/stable
+    public static final String DEFAULT_DCAE_JARS_INDEX_URL = "";
+
+    // cluster common defaults
+    public static final String DEFAULT_CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL = "5 sec";
+    public static final String DEFAULT_CLUSTER_PROTOCOL_MULTICAST_SERVICE_BROADCAST_DELAY = "500 ms";
+    public static final int DEFAULT_CLUSTER_PROTOCOL_MULTICAST_SERVICE_LOCATOR_ATTEMPTS = 3;
+    public static final String DEFAULT_CLUSTER_PROTOCOL_MULTICAST_SERVICE_LOCATOR_ATTEMPTS_DELAY = "1 sec";
+    public static final String DEFAULT_CLUSTER_NODE_READ_TIMEOUT = "5 sec";
+    public static final String DEFAULT_CLUSTER_NODE_CONNECTION_TIMEOUT = "5 sec";
+    public static final int DEFAULT_CLUSTER_NODE_MAX_CONCURRENT_REQUESTS = 100;
+
+    // cluster node defaults
+    public static final int DEFAULT_CLUSTER_NODE_PROTOCOL_THREADS = 10;
+    public static final int DEFAULT_CLUSTER_NODE_PROTOCOL_MAX_THREADS = 50;
+    public static final String DEFAULT_REQUEST_REPLICATION_CLAIM_TIMEOUT = "15 secs";
+    public static final String DEFAULT_FLOW_ELECTION_MAX_WAIT_TIME = "5 mins";
+
+    // cluster load balance defaults
+    public static final int DEFAULT_LOAD_BALANCE_PORT = 6342;
+    public static final int DEFAULT_LOAD_BALANCE_CONNECTIONS_PER_NODE = 4;
+    public static final int DEFAULT_LOAD_BALANCE_MAX_THREAD_COUNT = 8;
+    public static final String DEFAULT_LOAD_BALANCE_COMMS_TIMEOUT = "30 sec";
+
+
+    // state management defaults
+    public static final String DEFAULT_STATE_MANAGEMENT_CONFIG_FILE = "conf/state-management.xml";
+
+    // Kerberos defaults
+    public static final String DEFAULT_KERBEROS_AUTHENTICATION_EXPIRATION = "12 hours";
+
+
+    /**
+     * Retrieves the property value for the given property key.
+     *
+     * @param key the key of property value to lookup
+     * @return value of property at given key or null if not found
+     */
+    public abstract String getProperty(String key);
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    public abstract Set<String> getPropertyKeys();
+
+    // getters for core properties //
+    public File getFlowConfigurationFile() {
+        try {
+            return new File(getProperty(FLOW_CONFIGURATION_FILE));
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    public File getFlowConfigurationFileDir() {
+        try {
+            return getFlowConfigurationFile().getParentFile();
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    private Integer getPropertyAsPort(final String propertyName, final Integer defaultValue) {
+        final String port = getProperty(propertyName);
+        if (StringUtils.isEmpty(port)) {
+            return defaultValue;
+        }
+        try {
+            final int val = Integer.parseInt(port);
+            if (val <= 0 || val > 65535) {
+                throw new RuntimeException("Valid port range is 0 - 65535 but got " + val);
+            }
+            return val;
+        } catch (final NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    public int getQueueSwapThreshold() {
+        final String thresholdValue = getProperty(QUEUE_SWAP_THRESHOLD);
+        if (thresholdValue == null) {
+            return DEFAULT_QUEUE_SWAP_THRESHOLD;
+        }
+
+        try {
+            return Integer.parseInt(thresholdValue);
+        } catch (final NumberFormatException e) {
+            return DEFAULT_QUEUE_SWAP_THRESHOLD;
+        }
+    }
+
+    public Integer getIntegerProperty(final String propertyName, final Integer defaultValue) {
+        final String value = getProperty(propertyName);
+        if (value == null || value.trim().isEmpty()) {
+            return defaultValue;
+        }
+
+        try {
+            return Integer.parseInt(value.trim());
+        } catch (final Exception e) {
+            return defaultValue;
+        }
+    }
+
+    public int getSwapInThreads() {
+        return getIntegerProperty(SWAP_IN_THREADS, DEFAULT_SWAP_IN_THREADS);
+    }
+
+    public int getSwapOutThreads() {
+        final String value = getProperty(SWAP_OUT_THREADS);
+        if (value == null) {
+            return DEFAULT_SWAP_OUT_THREADS;
+        }
+
+        try {
+            return Integer.parseInt(getProperty(SWAP_OUT_THREADS));
+        } catch (final Exception e) {
+            return DEFAULT_SWAP_OUT_THREADS;
+        }
+    }
+
+    public String getSwapInPeriod() {
+        return getProperty(SWAP_IN_PERIOD, DEFAULT_SWAP_IN_PERIOD);
+    }
+
+    public String getSwapOutPeriod() {
+        return getProperty(SWAP_OUT_PERIOD, DEFAULT_SWAP_OUT_PERIOD);
+    }
+
+    public String getAdministrativeYieldDuration() {
+        return getProperty(ADMINISTRATIVE_YIELD_DURATION, DEFAULT_ADMINISTRATIVE_YIELD_DURATION);
+    }
+
+    /**
+     * The host name that will be given out to clients to connect to the Remote
+     * Input Port.
+     *
+     * @return the remote input host name or null if not configured
+     */
+    public String getRemoteInputHost() {
+        final String value = getProperty(REMOTE_INPUT_HOST);
+        return StringUtils.isBlank(value) ? null : value;
+    }
+
+    /**
+     * The socket port to listen on for a Remote Input Port.
+     *
+     * @return the remote input port for RAW socket communication
+     */
+    public Integer getRemoteInputPort() {
+        return getPropertyAsPort(REMOTE_INPUT_PORT, DEFAULT_REMOTE_INPUT_PORT);
+    }
+
+    /**
+     * @return False if property value is 'false'; True otherwise.
+     */
+    public Boolean isSiteToSiteSecure() {
+        final String secureVal = getProperty(SITE_TO_SITE_SECURE, "true");
+
+        return !"false".equalsIgnoreCase(secureVal);
+
+    }
+
+    /**
+     * @return True if property value is 'true'; False otherwise.
+     */
+    public Boolean isSiteToSiteHttpEnabled() {
+        final String remoteInputHttpEnabled = getProperty(SITE_TO_SITE_HTTP_ENABLED, "false");
+
+        return "true".equalsIgnoreCase(remoteInputHttpEnabled);
+
+    }
+
+    /**
+     * The HTTP or HTTPS Web API port for a Remote Input Port.
+     *
+     * @return the remote input port for HTTP(S) communication, or null if
+     * HTTP(S) Site-to-Site is not enabled
+     */
+    public Integer getRemoteInputHttpPort() {
+        if (!isSiteToSiteHttpEnabled()) {
+            return null;
+        }
+
+        final String propertyKey;
+        if (isSiteToSiteSecure()) {
+            if (StringUtils.isBlank(getProperty(NiFiProperties.WEB_HTTPS_PORT_FORWARDING))) {
+                propertyKey = WEB_HTTPS_PORT;
+            } else {
+                propertyKey = WEB_HTTPS_PORT_FORWARDING;
+            }
+        } else {
+            if (StringUtils.isBlank(getProperty(NiFiProperties.WEB_HTTP_PORT_FORWARDING))) {
+                propertyKey = WEB_HTTP_PORT;
+            } else {
+                propertyKey = WEB_HTTP_PORT_FORWARDING;
+            }
+        }
+
+        final Integer port = getIntegerProperty(propertyKey, null);
+        if (port == null) {
+            throw new RuntimeException("Remote input HTTP" + (isSiteToSiteSecure() ? "S" : "")
+                    + " is enabled but " + propertyKey + " is not specified.");
+        }
+        return port;
+    }
+
+    /**
+     * Returns the directory to which Templates are to be persisted
+     *
+     * @return the template directory
+     */
+    public Path getTemplateDirectory() {
+        final String strVal = getProperty(TEMPLATE_DIRECTORY);
+        return (strVal == null) ? DEFAULT_TEMPLATE_DIRECTORY : Paths.get(strVal);
+    }
+
+    /**
+     * Get the flow service write delay.
+     *
+     * @return The write delay
+     */
+    public String getFlowServiceWriteDelay() {
+        return getProperty(WRITE_DELAY_INTERVAL);
+    }
+
+    /**
+     * Returns whether the processors should be started automatically when the
+     * application loads.
+     *
+     * @return Whether to auto start the processors or not
+     */
+    public boolean getAutoResumeState() {
+        final String rawAutoResumeState = getProperty(AUTO_RESUME_STATE,
+                DEFAULT_AUTO_RESUME_STATE.toString());
+        return Boolean.parseBoolean(rawAutoResumeState);
+    }
+
+    /**
+     * Returns the number of partitions that should be used for the FlowFile
+     * Repository
+     *
+     * @return the number of partitions
+     */
+    public int getFlowFileRepositoryPartitions() {
+        final String rawProperty = getProperty(FLOWFILE_REPOSITORY_PARTITIONS,
+                DEFAULT_FLOWFILE_REPO_PARTITIONS);
+        return Integer.parseInt(rawProperty);
+    }
+
+    /**
+     * Returns the number of milliseconds between FlowFileRepository
+     * checkpointing
+     *
+     * @return the number of milliseconds between checkpoint events
+     */
+    public String getFlowFileRepositoryCheckpointInterval() {
+        return getProperty(FLOWFILE_REPOSITORY_CHECKPOINT_INTERVAL,
+                DEFAULT_FLOWFILE_CHECKPOINT_INTERVAL);
+    }
+
+    /**
+     * @return the restore directory or null if not configured
+     */
+    public File getRestoreDirectory() {
+        final String value = getProperty(RESTORE_DIRECTORY);
+        if (StringUtils.isBlank(value)) {
+            return null;
+        } else {
+            return new File(value);
+        }
+    }
+
+    /**
+     * @return the user authorizers file
+     */
+    public File getAuthorizerConfigurationFile() {
+        final String value = getProperty(AUTHORIZER_CONFIGURATION_FILE);
+        if (StringUtils.isBlank(value)) {
+            return new File(DEFAULT_AUTHORIZER_CONFIGURATION_FILE);
+        } else {
+            return new File(value);
+        }
+    }
+
+    /**
+     * @return the user login identity provider file
+     */
+    public File getLoginIdentityProviderConfigurationFile() {
+        final String value = getProperty(LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE);
+        if (StringUtils.isBlank(value)) {
+            return new File(DEFAULT_LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE);
+        } else {
+            return new File(value);
+        }
+    }
+
+    // getters for web properties //
+    public Integer getPort() {
+        Integer port = null;
+        try {
+            port = Integer.parseInt(getProperty(WEB_HTTP_PORT));
+        } catch (NumberFormatException nfe) {
+        }
+        return port;
+    }
+
+    public Integer getSslPort() {
+        Integer sslPort = null;
+        try {
+            sslPort = Integer.parseInt(getProperty(WEB_HTTPS_PORT));
+        } catch (NumberFormatException nfe) {
+        }
+        return sslPort;
+    }
+
+    public boolean isHTTPSConfigured() {
+        return getSslPort() != null;
+    }
+
+    /**
+     * Determines the HTTP/HTTPS port NiFi is configured to bind to. Prefers the HTTPS port. Throws an exception if neither is configured.
+     *
+     * @return the configured port number
+     */
+    public Integer getConfiguredHttpOrHttpsPort() throws RuntimeException {
+        if (getSslPort() != null) {
+            return getSslPort();
+        } else if (getPort() != null) {
+            return getPort();
+        } else {
+            throw new RuntimeException("The HTTP or HTTPS port must be configured");
+        }
+    }
+
+    public String getWebMaxHeaderSize() {
+        return getProperty(WEB_MAX_HEADER_SIZE, DEFAULT_WEB_MAX_HEADER_SIZE);
+    }
+
+    public int getWebThreads() {
+        return getIntegerProperty(WEB_THREADS, DEFAULT_WEB_THREADS);
+    }
+
+    public int getClusterNodeMaxConcurrentRequests() {
+        return getIntegerProperty(CLUSTER_NODE_MAX_CONCURRENT_REQUESTS, DEFAULT_CLUSTER_NODE_MAX_CONCURRENT_REQUESTS);
+    }
+
+    public File getWebWorkingDirectory() {
+        return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR));
+    }
+
+    public File getComponentDocumentationWorkingDirectory() {
+        return new File(getProperty(COMPONENT_DOCS_DIRECTORY, DEFAULT_COMPONENT_DOCS_DIRECTORY));
+    }
+
+    public File getNarWorkingDirectory() {
+        return new File(getProperty(NAR_WORKING_DIRECTORY, DEFAULT_NAR_WORKING_DIR));
+    }
+
+    public File getFrameworkWorkingDirectory() {
+        return new File(getNarWorkingDirectory(), "framework");
+    }
+
+    public File getExtensionsWorkingDirectory() {
+        return new File(getNarWorkingDirectory(), "extensions");
+    }
+
+    public List<Path> getNarLibraryDirectories() {
+
+        List<Path> narLibraryPaths = new ArrayList<>();
+
+        // go through each property
+        for (String propertyName : getPropertyKeys()) {
+            // determine if the property is a nar library path
+            if (StringUtils.startsWith(propertyName, NAR_LIBRARY_DIRECTORY_PREFIX)
+                    || NAR_LIBRARY_DIRECTORY.equals(propertyName)
+                    || NAR_LIBRARY_AUTOLOAD_DIRECTORY.equals(propertyName)) {
+                // attempt to resolve the path specified
+                String narLib = getProperty(propertyName);
+                if (!StringUtils.isBlank(narLib)) {
+                    narLibraryPaths.add(Paths.get(narLib));
+                }
+            }
+        }
+
+        if (narLibraryPaths.isEmpty()) {
+            narLibraryPaths.add(Paths.get(DEFAULT_NAR_LIBRARY_DIR));
+        }
+
+        return narLibraryPaths;
+    }
+
+    public File getNarAutoLoadDirectory() {
+        return new File(getProperty(NAR_LIBRARY_AUTOLOAD_DIRECTORY, DEFAULT_NAR_LIBRARY_AUTOLOAD_DIR));
+    }
+
+    /**
+     * Retrieves a URI to the index that contains URLs to all the DCAE jars to be loaded into Nifi.
+     * Refer to the genprocessor project for more info.
+     *
+     * Not setting the underlying configuration parameter "nifi.dcae.jar.index.url" is not
+     * fatal. Nifi will just skip over trying to load DCAE jars.
+     *
+     * @return
+     * @throws URISyntaxException
+     */
+    public URI getDCAEJarIndexURI() throws URISyntaxException {
+        String strUrl = getProperty(DCAE_JARS_INDEX_URL, DEFAULT_DCAE_JARS_INDEX_URL);
+
+        if (strUrl == null || strUrl.isEmpty()) {
+            return null;
+        } else {
+            return new URI(strUrl);
+        }
+    }
+
+    // getters for ui properties //
+
+    /**
+     * Get the banner text.
+     *
+     * @return The banner text
+     */
+    public String getBannerText() {
+        return this.getProperty(UI_BANNER_TEXT, StringUtils.EMPTY);
+    }
+
+
+    /**
+     * @author Renu
+     * @return the IP address where the nifi-app is being hosted
+     */
+    public String getDcaeDistributorApiHostname() {
+        return getProperty(UI_DCAE_DISTRIBUTOR_API_URL);
+    }
+
+    /**
+     * Returns the auto refresh interval in seconds.
+     *
+     * @return the interval over which the properties should auto refresh
+     */
+    public String getAutoRefreshInterval() {
+        return getProperty(UI_AUTO_REFRESH_INTERVAL);
+    }
+
+    // getters for cluster protocol properties //
+    public String getClusterProtocolHeartbeatInterval() {
+        return getProperty(CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL,
+                DEFAULT_CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL);
+    }
+
+    public String getNodeHeartbeatInterval() {
+        return getClusterProtocolHeartbeatInterval();
+    }
+
+    public String getClusterNodeReadTimeout() {
+        return getProperty(CLUSTER_NODE_READ_TIMEOUT, DEFAULT_CLUSTER_NODE_READ_TIMEOUT);
+    }
+
+    public String getClusterNodeConnectionTimeout() {
+        return getProperty(CLUSTER_NODE_CONNECTION_TIMEOUT,
+                DEFAULT_CLUSTER_NODE_CONNECTION_TIMEOUT);
+    }
+
+    public File getPersistentStateDirectory() {
+        final String dirName = getProperty(PERSISTENT_STATE_DIRECTORY,
+                DEFAULT_PERSISTENT_STATE_DIRECTORY);
+        final File file = new File(dirName);
+        if (!file.exists()) {
+            file.mkdirs();
+        }
+        return file;
+    }
+
+    // getters for cluster node properties //
+    public boolean isNode() {
+        return Boolean.parseBoolean(getProperty(CLUSTER_IS_NODE));
+    }
+
+    public InetSocketAddress getClusterNodeProtocolAddress() {
+        try {
+            String socketAddress = getProperty(CLUSTER_NODE_ADDRESS);
+            if (StringUtils.isBlank(socketAddress)) {
+                socketAddress = "localhost";
+            }
+            int socketPort = getClusterNodeProtocolPort();
+            return InetSocketAddress.createUnresolved(socketAddress, socketPort);
+        } catch (Exception ex) {
+            throw new RuntimeException("Invalid node protocol address/port due to: " + ex, ex);
+        }
+    }
+
+    public InetSocketAddress getClusterLoadBalanceAddress() {
+        try {
+            String address = getProperty(LOAD_BALANCE_ADDRESS);
+            if (StringUtils.isBlank(address)) {
+                address = getProperty(CLUSTER_NODE_ADDRESS);
+            }
+            if (StringUtils.isBlank(address)) {
+                address = "localhost";
+            }
+
+            final int port = getIntegerProperty(LOAD_BALANCE_PORT, DEFAULT_LOAD_BALANCE_PORT);
+            return InetSocketAddress.createUnresolved(address, port);
+        } catch (final Exception e) {
+            throw new RuntimeException("Invalid load balance address/port due to: " + e, e);
+        }
+    }
+
+    public Integer getClusterNodeProtocolPort() {
+        try {
+            return Integer.parseInt(getProperty(CLUSTER_NODE_PROTOCOL_PORT));
+        } catch (NumberFormatException nfe) {
+            return null;
+        }
+    }
+
+    /**
+     * @deprecated Use getClusterNodeProtocolCorePoolSize() and getClusterNodeProtocolMaxPoolSize() instead
+     */
+    @Deprecated()
+    public int getClusterNodeProtocolThreads() {
+        return getClusterNodeProtocolCorePoolSize();
+    }
+
+    public int getClusterNodeProtocolCorePoolSize() {
+        try {
+            return Integer.parseInt(getProperty(CLUSTER_NODE_PROTOCOL_THREADS));
+        } catch (NumberFormatException nfe) {
+            return DEFAULT_CLUSTER_NODE_PROTOCOL_THREADS;
+        }
+    }
+
+    public int getClusterNodeProtocolMaxPoolSize() {
+        try {
+            return Integer.parseInt(getProperty(CLUSTER_NODE_PROTOCOL_MAX_THREADS));
+        } catch (NumberFormatException nfe) {
+            return DEFAULT_CLUSTER_NODE_PROTOCOL_MAX_THREADS;
+        }
+    }
+
+    public boolean isClustered() {
+        return Boolean.parseBoolean(getProperty(CLUSTER_IS_NODE));
+    }
+
+    public File getClusterNodeFirewallFile() {
+        final String firewallFile = getProperty(CLUSTER_FIREWALL_FILE);
+        if (StringUtils.isBlank(firewallFile)) {
+            return null;
+        } else {
+            return new File(firewallFile);
+        }
+    }
+
+    public String getClusterProtocolManagerToNodeApiScheme() {
+        final String isSecureProperty = getProperty(CLUSTER_PROTOCOL_IS_SECURE);
+        if (Boolean.valueOf(isSecureProperty)) {
+            return "https";
+        } else {
+            return "http";
+        }
+    }
+
+    public File getKerberosConfigurationFile() {
+        final String krb5File = getProperty(KERBEROS_KRB5_FILE);
+        if (krb5File != null && krb5File.trim().length() > 0) {
+            return new File(krb5File.trim());
+        } else {
+            return null;
+        }
+    }
+
+    public String getKerberosServicePrincipal() {
+        final String servicePrincipal = getProperty(KERBEROS_SERVICE_PRINCIPAL);
+        if (!StringUtils.isBlank(servicePrincipal)) {
+            return servicePrincipal.trim();
+        } else {
+            return null;
+        }
+    }
+
+    public String getKerberosServiceKeytabLocation() {
+        final String keytabLocation = getProperty(KERBEROS_SERVICE_KEYTAB_LOCATION);
+        if (!StringUtils.isBlank(keytabLocation)) {
+            return keytabLocation.trim();
+        } else {
+            return null;
+        }
+    }
+
+    public String getKerberosSpnegoPrincipal() {
+        final String spengoPrincipal = getProperty(KERBEROS_SPNEGO_PRINCIPAL);
+        if (!StringUtils.isBlank(spengoPrincipal)) {
+            return spengoPrincipal.trim();
+        } else {
+            return null;
+        }
+    }
+
+    public String getKerberosSpnegoKeytabLocation() {
+        final String keytabLocation = getProperty(KERBEROS_SPNEGO_KEYTAB_LOCATION);
+        if (!StringUtils.isBlank(keytabLocation)) {
+            return keytabLocation.trim();
+        } else {
+            return null;
+        }
+    }
+
+    public String getKerberosAuthenticationExpiration() {
+        final String authenticationExpirationString = getProperty(KERBEROS_AUTHENTICATION_EXPIRATION, DEFAULT_KERBEROS_AUTHENTICATION_EXPIRATION);
+        if (!StringUtils.isBlank(authenticationExpirationString)) {
+            return authenticationExpirationString.trim();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns true if the Kerberos service principal and keytab location
+     * properties are populated.
+     *
+     * @return true if Kerberos service support is enabled
+     */
+    public boolean isKerberosSpnegoSupportEnabled() {
+        return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation());
+    }
+
+    /**
+     * Returns true if the login identity provider has been configured.
+     *
+     * @return true if the login identity provider has been configured
+     */
+    public boolean isLoginIdentityProviderEnabled() {
+        return !StringUtils.isBlank(getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER));
+    }
+
+    /**
+     * Returns whether an OpenId Connect (OIDC) URL is set.
+     *
+     * @return whether an OpenId Connection URL is set
+     */
+    public boolean isOidcEnabled() {
+        return !StringUtils.isBlank(getOidcDiscoveryUrl());
+    }
+
+    /**
+     * Returns the OpenId Connect (OIDC) URL. Null otherwise.
+     *
+     * @return OIDC discovery url
+     */
+    public String getOidcDiscoveryUrl() {
+        return getProperty(SECURITY_USER_OIDC_DISCOVERY_URL);
+    }
+
+    /**
+     * Returns the OpenId Connect connect timeout. Non null.
+     *
+     * @return OIDC connect timeout
+     */
+    public String getOidcConnectTimeout() {
+        return getProperty(SECURITY_USER_OIDC_CONNECT_TIMEOUT, DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
+    }
+
+    /**
+     * Returns the OpenId Connect read timeout. Non null.
+     *
+     * @return OIDC read timeout
+     */
+    public String getOidcReadTimeout() {
+        return getProperty(SECURITY_USER_OIDC_READ_TIMEOUT, DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
+    }
+
+    /**
+     * Returns the OpenId Connect client id.
+     *
+     * @return OIDC client id
+     */
+    public String getOidcClientId() {
+        return getProperty(SECURITY_USER_OIDC_CLIENT_ID);
+    }
+
+    /**
+     * Returns the OpenId Connect client secret.
+     *
+     * @return OIDC client secret
+     */
+    public String getOidcClientSecret() {
+        return getProperty(SECURITY_USER_OIDC_CLIENT_SECRET);
+    }
+
+    /**
+     * Returns the preferred json web signature algorithm. May be null/blank.
+     *
+     * @return OIDC preferred json web signature algorithm
+     */
+    public String getOidcPreferredJwsAlgorithm() {
+        return getProperty(SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM);
+    }
+
+    /**
+     * Returns whether Knox SSO is enabled.
+     *
+     * @return whether Knox SSO is enabled
+     */
+    public boolean isKnoxSsoEnabled() {
+        return !StringUtils.isBlank(getKnoxUrl());
+    }
+
+    /**
+     * Returns the Knox URL.
+     *
+     * @return Knox URL
+     */
+    public String getKnoxUrl() {
+        return getProperty(SECURITY_USER_KNOX_URL);
+    }
+
+    /**
+     * Gets the configured Knox Audiences.
+     *
+     * @return Knox audiences
+     */
+    public Set<String> getKnoxAudiences() {
+        final String rawAudiences = getProperty(SECURITY_USER_KNOX_AUDIENCES);
+        if (StringUtils.isBlank(rawAudiences)) {
+            return null;
+        } else {
+            final String[] audienceTokens = rawAudiences.split(",");
+            return Stream.of(audienceTokens).map(String::trim).filter(aud -> !StringUtils.isEmpty(aud)).collect(Collectors.toSet());
+        }
+    }
+
+    /**
+     * Returns the path to the Knox public key.
+     *
+     * @return path to the Knox public key
+     */
+    public Path getKnoxPublicKeyPath() {
+        return Paths.get(getProperty(SECURITY_USER_KNOX_PUBLIC_KEY));
+    }
+
+    /**
+     * Returns the name of the Knox cookie.
+     *
+     * @return name of the Knox cookie
+     */
+    public String getKnoxCookieName() {
+        return getProperty(SECURITY_USER_KNOX_COOKIE_NAME);
+    }
+
+    /**
+     * Returns true if client certificates are required for REST API. Determined
+     * if the following conditions are all true:
+     * <p>
+     * - login identity provider is not populated
+     * - Kerberos service support is not enabled
+     * - openid connect is not enabled
+     * - knox sso is not enabled
+     * </p>
+     *
+     * @return true if client certificates are required for access to the REST API
+     */
+    public boolean isClientAuthRequiredForRestApi() {
+        return !isLoginIdentityProviderEnabled() && !isKerberosSpnegoSupportEnabled() && !isOidcEnabled() && !isKnoxSsoEnabled();
+    }
+
+    public InetSocketAddress getNodeApiAddress() {
+
+        final String rawScheme = getClusterProtocolManagerToNodeApiScheme();
+        final String scheme = (rawScheme == null) ? "http" : rawScheme;
+
+        final String host;
+        final Integer port;
+        if ("http".equalsIgnoreCase(scheme)) {
+            // get host
+            if (StringUtils.isBlank(getProperty(WEB_HTTP_HOST))) {
+                host = "localhost";
+            } else {
+                host = getProperty(WEB_HTTP_HOST);
+            }
+            // get port
+            port = getPort();
+
+            if (port == null) {
+                throw new RuntimeException(String.format("The %s must be specified if running in a cluster with %s set to false.", WEB_HTTP_PORT, CLUSTER_PROTOCOL_IS_SECURE));
+            }
+        } else {
+            // get host
+            if (StringUtils.isBlank(getProperty(WEB_HTTPS_HOST))) {
+                host = "localhost";
+            } else {
+                host = getProperty(WEB_HTTPS_HOST);
+            }
+            // get port
+            port = getSslPort();
+
+            if (port == null) {
+                throw new RuntimeException(String.format("The %s must be specified if running in a cluster with %s set to true.", WEB_HTTPS_PORT, CLUSTER_PROTOCOL_IS_SECURE));
+            }
+        }
+
+        return InetSocketAddress.createUnresolved(host, port);
+
+    }
+
+    /**
+     * Returns the database repository path. It simply returns the value
+     * configured. No directories will be created as a result of this operation.
+     *
+     * @return database repository path
+     * @throws InvalidPathException If the configured path is invalid
+     */
+    public Path getDatabaseRepositoryPath() {
+        return Paths.get(getProperty(REPOSITORY_DATABASE_DIRECTORY));
+    }
+
+    /**
+     * Returns the flow file repository path. It simply returns the value
+     * configured. No directories will be created as a result of this operation.
+     *
+     * @return database repository path
+     * @throws InvalidPathException If the configured path is invalid
+     */
+    public Path getFlowFileRepositoryPath() {
+        return Paths.get(getProperty(FLOWFILE_REPOSITORY_DIRECTORY));
+    }
+
+    /**
+     * Returns the content repository paths. This method returns a mapping of
+     * file repository name to file repository paths. It simply returns the
+     * values configured. No directories will be created as a result of this
+     * operation.
+     *
+     * @return file repositories paths
+     * @throws InvalidPathException If any of the configured paths are invalid
+     */
+    public Map<String, Path> getContentRepositoryPaths() {
+        final Map<String, Path> contentRepositoryPaths = new HashMap<>();
+
+        // go through each property
+        for (String propertyName : getPropertyKeys()) {
+            // determine if the property is a file repository path
+            if (StringUtils.startsWith(propertyName, REPOSITORY_CONTENT_PREFIX)) {
+                // get the repository key
+                final String key = StringUtils.substringAfter(propertyName,
+                        REPOSITORY_CONTENT_PREFIX);
+
+                // attempt to resolve the path specified
+                contentRepositoryPaths.put(key, Paths.get(getProperty(propertyName)));
+            }
+        }
+        return contentRepositoryPaths;
+    }
+
+    /**
+     * Returns the provenance repository paths. This method returns a mapping of
+     * file repository name to file repository paths. It simply returns the
+     * values configured. No directories will be created as a result of this
+     * operation.
+     *
+     * @return the name and paths of all provenance repository locations
+     */
+    public Map<String, Path> getProvenanceRepositoryPaths() {
+        final Map<String, Path> provenanceRepositoryPaths = new HashMap<>();
+
+        // go through each property
+        for (String propertyName : getPropertyKeys()) {
+            // determine if the property is a file repository path
+            if (StringUtils.startsWith(propertyName, PROVENANCE_REPO_DIRECTORY_PREFIX)) {
+                // get the repository key
+                final String key = StringUtils.substringAfter(propertyName,
+                        PROVENANCE_REPO_DIRECTORY_PREFIX);
+
+                // attempt to resolve the path specified
+                provenanceRepositoryPaths.put(key, Paths.get(getProperty(propertyName)));
+            }
+        }
+        return provenanceRepositoryPaths;
+    }
+
+    /**
+     * Returns the number of claims to keep open for writing. Ideally, this will be at
+     * least as large as the number of threads that will be updating the repository simultaneously but we don't want
+     * to get too large because it will hold open up to this many FileOutputStreams.
+     * <p>
+     * Default is {@link #DEFAULT_MAX_FLOWFILES_PER_CLAIM}
+     *
+     * @return the maximum number of flow files per claim
+     */
+    public int getMaxFlowFilesPerClaim() {
+        try {
+            return Integer.parseInt(getProperty(MAX_FLOWFILES_PER_CLAIM));
+        } catch (NumberFormatException nfe) {
+            return DEFAULT_MAX_FLOWFILES_PER_CLAIM;
+        }
+    }
+
+    /**
+     * Returns the maximum size, in bytes, that claims should grow before writing a new file. This means that we won't continually write to one
+     * file that keeps growing but gives us a chance to bunch together many small files.
+     * <p>
+     * Default is {@link #DEFAULT_MAX_APPENDABLE_CLAIM_SIZE}
+     *
+     * @return the maximum appendable claim size
+     */
+    public String getMaxAppendableClaimSize() {
+        return getProperty(MAX_APPENDABLE_CLAIM_SIZE, DEFAULT_MAX_APPENDABLE_CLAIM_SIZE);
+    }
+
+    public String getProperty(final String key, final String defaultValue) {
+        final String value = getProperty(key);
+        return (value == null || value.trim().isEmpty()) ? defaultValue : value;
+    }
+
+    public String getBoredYieldDuration() {
+        return getProperty(BORED_YIELD_DURATION, DEFAULT_BORED_YIELD_DURATION);
+    }
+
+    public File getStateManagementConfigFile() {
+        return new File(getProperty(STATE_MANAGEMENT_CONFIG_FILE, DEFAULT_STATE_MANAGEMENT_CONFIG_FILE));
+    }
+
+    public String getLocalStateProviderId() {
+        return getProperty(STATE_MANAGEMENT_LOCAL_PROVIDER_ID);
+    }
+
+    public String getClusterStateProviderId() {
+        return getProperty(STATE_MANAGEMENT_CLUSTER_PROVIDER_ID);
+    }
+
+    public File getEmbeddedZooKeeperPropertiesFile() {
+        final String filename = getProperty(STATE_MANAGEMENT_ZOOKEEPER_PROPERTIES);
+        return filename == null ? null : new File(filename);
+    }
+
+    public boolean isStartEmbeddedZooKeeper() {
+        return Boolean.parseBoolean(getProperty(STATE_MANAGEMENT_START_EMBEDDED_ZOOKEEPER));
+    }
+
+    public boolean isFlowConfigurationArchiveEnabled() {
+        return Boolean.parseBoolean(getProperty(FLOW_CONFIGURATION_ARCHIVE_ENABLED, DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED));
+    }
+
+    public String getFlowConfigurationArchiveDir() {
+        return getProperty(FLOW_CONFIGURATION_ARCHIVE_DIR);
+    }
+
+    public String getFlowElectionMaxWaitTime() {
+        return getProperty(FLOW_ELECTION_MAX_WAIT_TIME, DEFAULT_FLOW_ELECTION_MAX_WAIT_TIME);
+    }
+
+    public Integer getFlowElectionMaxCandidates() {
+        return getIntegerProperty(FLOW_ELECTION_MAX_CANDIDATES, null);
+    }
+
+    public String getFlowConfigurationArchiveMaxTime() {
+        return getProperty(FLOW_CONFIGURATION_ARCHIVE_MAX_TIME, null);
+    }
+
+    public String getFlowConfigurationArchiveMaxStorage() {
+        return getProperty(FLOW_CONFIGURATION_ARCHIVE_MAX_STORAGE, null);
+    }
+
+    public Integer getFlowConfigurationArchiveMaxCount() {
+        return getIntegerProperty(FLOW_CONFIGURATION_ARCHIVE_MAX_COUNT, null);
+    }
+
+    public String getVariableRegistryProperties() {
+        return getProperty(VARIABLE_REGISTRY_PROPERTIES);
+    }
+
+    public Path[] getVariableRegistryPropertiesPaths() {
+        final List<Path> vrPropertiesPaths = new ArrayList<>();
+
+        final String vrPropertiesFiles = getVariableRegistryProperties();
+        if (!StringUtils.isEmpty(vrPropertiesFiles)) {
+
+            final List<String> vrPropertiesFileList = Arrays.asList(vrPropertiesFiles.split(","));
+
+            for (String propertiesFile : vrPropertiesFileList) {
+                vrPropertiesPaths.add(Paths.get(propertiesFile));
+            }
+
+            return vrPropertiesPaths.toArray(new Path[vrPropertiesPaths.size()]);
+        } else {
+            return new Path[]{};
+        }
+    }
+
+    /**
+     * Returns the network interface list to use for HTTP. This method returns a mapping of
+     * network interface property names to network interface names.
+     *
+     * @return the property name and network interface name of all HTTP network interfaces
+     */
+    public Map<String, String> getHttpNetworkInterfaces() {
+        final Map<String, String> networkInterfaces = new HashMap<>();
+
+        // go through each property
+        for (String propertyName : getPropertyKeys()) {
+            // determine if the property is a network interface name
+            if (StringUtils.startsWith(propertyName, WEB_HTTP_NETWORK_INTERFACE_PREFIX)) {
+                // get the network interface property key
+                final String key = StringUtils.substringAfter(propertyName,
+                        WEB_HTTP_NETWORK_INTERFACE_PREFIX);
+                networkInterfaces.put(key, getProperty(propertyName));
+            }
+        }
+        return networkInterfaces;
+    }
+
+    /**
+     * Returns the network interface list to use for HTTPS. This method returns a mapping of
+     * network interface property names to network interface names.
+     *
+     * @return the property name and network interface name of all HTTPS network interfaces
+     */
+    public Map<String, String> getHttpsNetworkInterfaces() {
+        final Map<String, String> networkInterfaces = new HashMap<>();
+
+        // go through each property
+        for (String propertyName : getPropertyKeys()) {
+            // determine if the property is a network interface name
+            if (StringUtils.startsWith(propertyName, WEB_HTTPS_NETWORK_INTERFACE_PREFIX)) {
+                // get the network interface property key
+                final String key = StringUtils.substringAfter(propertyName,
+                        WEB_HTTPS_NETWORK_INTERFACE_PREFIX);
+                networkInterfaces.put(key, getProperty(propertyName));
+            }
+        }
+        return networkInterfaces;
+    }
+
+    public int size() {
+        return getPropertyKeys().size();
+    }
+
+    public String getProvenanceRepoEncryptionKeyId() {
+        return getProperty(PROVENANCE_REPO_ENCRYPTION_KEY_ID);
+    }
+
+    /**
+     * Returns the active provenance repository encryption key if a {@code StaticKeyProvider} is in use.
+     * If no key ID is specified in the properties file, the default
+     * {@code nifi.provenance.repository.encryption.key} value is returned. If a key ID is specified in
+     * {@code nifi.provenance.repository.encryption.key.id}, it will attempt to read from
+     * {@code nifi.provenance.repository.encryption.key.id.XYZ} where {@code XYZ} is the provided key
+     * ID. If that value is empty, it will use the default property
+     * {@code nifi.provenance.repository.encryption.key}.
+     *
+     * @return the provenance repository encryption key in hex form
+     */
+    public String getProvenanceRepoEncryptionKey() {
+        String keyId = getProvenanceRepoEncryptionKeyId();
+        String keyKey = StringUtils.isBlank(keyId) ? PROVENANCE_REPO_ENCRYPTION_KEY : PROVENANCE_REPO_ENCRYPTION_KEY + ".id." + keyId;
+        return getProperty(keyKey, getProperty(PROVENANCE_REPO_ENCRYPTION_KEY));
+    }
+
+    /**
+     * Returns a map of keyId -> key in hex loaded from the {@code nifi.properties} file if a
+     * {@code StaticKeyProvider} is defined. If {@code FileBasedKeyProvider} is defined, use
+     * {@code CryptoUtils#readKeys()} instead -- this method will return an empty map.
+     *
+     * @return a Map of the keys identified by key ID
+     */
+    public Map<String, String> getProvenanceRepoEncryptionKeys() {
+        Map<String, String> keys = new HashMap<>();
+        List<String> keyProperties = getProvenanceRepositoryEncryptionKeyProperties();
+
+        // Retrieve the actual key values and store non-empty values in the map
+        for (String prop : keyProperties) {
+            final String value = getProperty(prop);
+            if (!StringUtils.isBlank(value)) {
+                if (prop.equalsIgnoreCase(PROVENANCE_REPO_ENCRYPTION_KEY)) {
+                    prop = getProvenanceRepoEncryptionKeyId();
+                } else {
+                    // Extract nifi.provenance.repository.encryption.key.id.key1 -> key1
+                    prop = prop.substring(prop.lastIndexOf(".") + 1);
+                }
+                keys.put(prop, value);
+            }
+        }
+        return keys;
+    }
+
+    /**
+     * Returns the whitelisted proxy hostnames (and IP addresses) as a comma-delimited string.
+     * The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}.
+     * <p>
+     * Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_HOST)} will not normalize the hosts.
+     *
+     * @return the hostname(s)
+     */
+    public String getWhitelistedHosts() {
+        return StringUtils.join(getWhitelistedHostsAsList(), ",");
+    }
+
+    /**
+     * Returns the whitelisted proxy hostnames (and IP addresses) as a List. The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}.
+     *
+     * @return the hostname(s)
+     */
+    public List<String> getWhitelistedHostsAsList() {
+        String rawProperty = getProperty(WEB_PROXY_HOST, "");
+        List<String> hosts = Arrays.asList(rawProperty.split(","));
+        return hosts.stream()
+                .map(this::normalizeHost).filter(host -> !StringUtils.isBlank(host)).collect(Collectors.toList());
+    }
+
+    String normalizeHost(String host) {
+        if (host == null || host.equalsIgnoreCase("")) {
+            return "";
+        } else {
+            return host.trim();
+        }
+    }
+
+    /**
+     * Returns the whitelisted proxy context paths as a comma-delimited string. The paths have been normalized to the form {@code /some/context/path}.
+     * <p>
+     * Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_CONTEXT_PATH)} will not normalize the paths.
+     *
+     * @return the path(s)
+     */
+    public String getWhitelistedContextPaths() {
+        return StringUtils.join(getWhitelistedContextPathsAsList(), ",");
+    }
+
+    /**
+     * Returns the whitelisted proxy context paths as a list of paths. The paths have been normalized to the form {@code /some/context/path}.
+     *
+     * @return the path(s)
+     */
+    public List<String> getWhitelistedContextPathsAsList() {
+        String rawProperty = getProperty(WEB_PROXY_CONTEXT_PATH, "");
+        List<String> contextPaths = Arrays.asList(rawProperty.split(","));
+        return contextPaths.stream()
+                .map(this::normalizeContextPath).collect(Collectors.toList());
+    }
+
+    private String normalizeContextPath(String cp) {
+        if (cp == null || cp.equalsIgnoreCase("")) {
+            return "";
+        } else {
+            String trimmedCP = cp.trim();
+            // Ensure it starts with a leading slash and does not end in a trailing slash
+            // There's a potential for the path to be something like bad/path/// but this is semi-trusted data from an admin-accessible file and there are way worse possibilities here
+            trimmedCP = trimmedCP.startsWith("/") ? trimmedCP : "/" + trimmedCP;
+            trimmedCP = trimmedCP.endsWith("/") ? trimmedCP.substring(0, trimmedCP.length() - 1) : trimmedCP;
+            return trimmedCP;
+        }
+    }
+
+    private List<String> getProvenanceRepositoryEncryptionKeyProperties() {
+        // Filter all the property keys that define a key
+        return getPropertyKeys().stream().filter(k ->
+                k.startsWith(PROVENANCE_REPO_ENCRYPTION_KEY_ID + ".") || k.equalsIgnoreCase(PROVENANCE_REPO_ENCRYPTION_KEY)
+        ).collect(Collectors.toList());
+    }
+
+    public Long getDefaultBackPressureObjectThreshold() {
+        long backPressureCount;
+        try {
+            String backPressureCountStr = getProperty(BACKPRESSURE_COUNT);
+            if (backPressureCountStr == null || backPressureCountStr.trim().isEmpty()) {
+                backPressureCount = DEFAULT_BACKPRESSURE_COUNT;
+            } else {
+                backPressureCount = Long.parseLong(backPressureCountStr);
+            }
+        } catch (NumberFormatException nfe) {
+            backPressureCount = DEFAULT_BACKPRESSURE_COUNT;
+        }
+        return backPressureCount;
+    }
+
+    public String getDefaultBackPressureDataSizeThreshold() {
+        return getProperty(BACKPRESSURE_SIZE, DEFAULT_BACKPRESSURE_SIZE);
+    }
+
+    /**
+     * Creates an instance of NiFiProperties. This should likely not be called
+     * by any classes outside of the NiFi framework but can be useful by the
+     * framework for default property loading behavior or helpful in tests
+     * needing to create specific instances of NiFiProperties. If properties
+     * file specified cannot be found/read a runtime exception will be thrown.
+     * If one is not specified no properties will be loaded by default.
+     *
+     * @param propertiesFilePath   if provided properties will be loaded from
+     *                             given file; else will be loaded from System property. Can be null.
+     * @param additionalProperties allows overriding of properties with the
+     *                             supplied values. these will be applied after loading from any properties
+     *                             file. Can be null or empty.
+     * @return NiFiProperties
+     */
+    public static NiFiProperties createBasicNiFiProperties(final String propertiesFilePath, final Map<String, String> additionalProperties) {
+        final Map<String, String> addProps = (additionalProperties == null) ? Collections.EMPTY_MAP : additionalProperties;
+        final Properties properties = new Properties();
+        final String nfPropertiesFilePath = (propertiesFilePath == null)
+                ? System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+                : propertiesFilePath;
+        if (nfPropertiesFilePath != null) {
+            final File propertiesFile = new File(nfPropertiesFilePath.trim());
+            if (!propertiesFile.exists()) {
+                throw new RuntimeException("Properties file doesn't exist \'"
+                        + propertiesFile.getAbsolutePath() + "\'");
+            }
+            if (!propertiesFile.canRead()) {
+                throw new RuntimeException("Properties file exists but cannot be read \'"
+                        + propertiesFile.getAbsolutePath() + "\'");
+            }
+            InputStream inStream = null;
+            try {
+                inStream = new BufferedInputStream(new FileInputStream(propertiesFile));
+                properties.load(inStream);
+            } catch (final Exception ex) {
+                throw new RuntimeException("Cannot load properties file due to "
+                        + ex.getLocalizedMessage(), ex);
+            } finally {
+                if (null != inStream) {
+                    try {
+                        inStream.close();
+                    } catch (final Exception ex) {
+                        /**
+                         * do nothing *
+                         */
+                    }
+                }
+            }
+        }
+        addProps.entrySet().stream().forEach((entry) -> {
+            properties.setProperty(entry.getKey(), entry.getValue());
+        });
+        return new NiFiProperties() {
+            @Override
+            public String getProperty(String key) {
+                return properties.getProperty(key);
+            }
+
+            @Override
+            public Set<String> getPropertyKeys() {
+                return properties.stringPropertyNames();
+            }
+        };
+    }
+
+    /**
+     * This method is used to validate the NiFi properties when the file is loaded
+     * for the first time. The objective is to stop NiFi startup in case a property
+     * is not correctly configured and could cause issues afterwards.
+     */
+    public void validate() {
+        // REMOTE_INPUT_HOST should be a valid hostname
+        String remoteInputHost = getProperty(REMOTE_INPUT_HOST);
+        if (!StringUtils.isBlank(remoteInputHost) && remoteInputHost.split(":").length > 1) { // no scheme/port needed here (http://)
+            throw new IllegalArgumentException(remoteInputHost + " is not a correct value for " + REMOTE_INPUT_HOST + ". It should be a valid hostname without protocol or port.");
+        }
+        // Other properties to validate...
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
new file mode 100644 (file)
index 0000000..8ad05bd
--- /dev/null
@@ -0,0 +1,4899 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.web;
+
+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;
+import org.apache.nifi.action.Operation;
+import org.apache.nifi.action.details.FlowChangePurgeDetails;
+import org.apache.nifi.admin.service.AuditService;
+import org.apache.nifi.authorization.AccessDeniedException;
+import org.apache.nifi.authorization.AccessPolicy;
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizationRequest;
+import org.apache.nifi.authorization.AuthorizationResult;
+import org.apache.nifi.authorization.AuthorizationResult.Result;
+import org.apache.nifi.authorization.AuthorizeAccess;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.Group;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.Resource;
+import org.apache.nifi.authorization.User;
+import org.apache.nifi.authorization.UserContextKeys;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.EnforcePolicyPermissionsThroughBaseResource;
+import org.apache.nifi.authorization.resource.OperationAuthorizable;
+import org.apache.nifi.authorization.resource.ResourceFactory;
+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.coordination.ClusterCoordinator;
+import org.apache.nifi.cluster.coordination.heartbeat.HeartbeatMonitor;
+import org.apache.nifi.cluster.coordination.heartbeat.NodeHeartbeat;
+import org.apache.nifi.cluster.coordination.node.ClusterRoles;
+import org.apache.nifi.cluster.coordination.node.DisconnectionCode;
+import org.apache.nifi.cluster.coordination.node.NodeConnectionState;
+import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus;
+import org.apache.nifi.cluster.coordination.node.OffloadCode;
+import org.apache.nifi.cluster.event.NodeEvent;
+import org.apache.nifi.cluster.manager.exception.IllegalNodeDeletionException;
+import org.apache.nifi.cluster.manager.exception.UnknownNodeException;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.RequiredPermission;
+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;
+import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.Counter;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ReportingTaskNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.Snippet;
+import org.apache.nifi.controller.Template;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.leader.election.LeaderElectionManager;
+import org.apache.nifi.controller.repository.claim.ContentDirection;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceReference;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.controller.status.ProcessGroupStatus;
+import org.apache.nifi.controller.status.ProcessorStatus;
+import org.apache.nifi.diagnostics.SystemDiagnostics;
+import org.apache.nifi.events.BulletinFactory;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.ProcessGroupCounts;
+import org.apache.nifi.groups.RemoteProcessGroup;
+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;
+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.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;
+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.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;
+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.BundleUtils;
+import org.apache.nifi.util.FlowDifferenceFilters;
+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;
+import org.apache.nifi.web.api.dto.BundleDTO;
+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.ComponentRestrictionPermissionDTO;
+import org.apache.nifi.web.api.dto.ComponentStateDTO;
+import org.apache.nifi.web.api.dto.ConnectionDTO;
+import org.apache.nifi.web.api.dto.ControllerConfigurationDTO;
+import org.apache.nifi.web.api.dto.ControllerDTO;
+import org.apache.nifi.web.api.dto.ControllerServiceDTO;
+import org.apache.nifi.web.api.dto.ControllerServiceReferencingComponentDTO;
+import org.apache.nifi.web.api.dto.CounterDTO;
+import org.apache.nifi.web.api.dto.CountersDTO;
+import org.apache.nifi.web.api.dto.CountersSnapshotDTO;
+import org.apache.nifi.web.api.dto.DocumentedTypeDTO;
+import org.apache.nifi.web.api.dto.DropRequestDTO;
+import org.apache.nifi.web.api.dto.DtoFactory;
+import org.apache.nifi.web.api.dto.EntityFactory;
+import org.apache.nifi.web.api.dto.FlowConfigurationDTO;
+import org.apache.nifi.web.api.dto.FlowFileDTO;
+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.ListingRequestDTO;
+import org.apache.nifi.web.api.dto.NodeDTO;
+import org.apache.nifi.web.api.dto.PermissionsDTO;
+import org.apache.nifi.web.api.dto.PortDTO;
+import org.apache.nifi.web.api.dto.PreviousValueDTO;
+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.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;
+import org.apache.nifi.web.api.dto.RequiredPermissionDTO;
+import org.apache.nifi.web.api.dto.ResourceDTO;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.SnippetDTO;
+import org.apache.nifi.web.api.dto.SystemDiagnosticsDTO;
+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.diagnostics.ConnectionDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ControllerServiceDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ProcessorDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.flow.FlowDTO;
+import org.apache.nifi.web.api.dto.provenance.ProvenanceDTO;
+import org.apache.nifi.web.api.dto.provenance.ProvenanceEventDTO;
+import org.apache.nifi.web.api.dto.provenance.ProvenanceOptionsDTO;
+import org.apache.nifi.web.api.dto.provenance.lineage.LineageDTO;
+import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
+import org.apache.nifi.web.api.dto.status.ConnectionStatusDTO;
+import org.apache.nifi.web.api.dto.status.ControllerStatusDTO;
+import org.apache.nifi.web.api.dto.status.NodeProcessGroupStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.PortStatusDTO;
+import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
+import org.apache.nifi.web.api.dto.status.ProcessGroupStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO;
+import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusDTO;
+import org.apache.nifi.web.api.dto.status.StatusHistoryDTO;
+import org.apache.nifi.web.api.entity.AccessPolicyEntity;
+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;
+import org.apache.nifi.web.api.entity.ConnectionStatusEntity;
+import org.apache.nifi.web.api.entity.ControllerBulletinsEntity;
+import org.apache.nifi.web.api.entity.ControllerConfigurationEntity;
+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;
+import org.apache.nifi.web.api.entity.LabelEntity;
+import org.apache.nifi.web.api.entity.PortEntity;
+import org.apache.nifi.web.api.entity.PortStatusEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
+import org.apache.nifi.web.api.entity.ProcessorDiagnosticsEntity;
+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;
+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;
+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.api.entity.VersionedFlowSnapshotMetadataEntity;
+import org.apache.nifi.web.controller.ControllerFacade;
+import org.apache.nifi.web.dao.AccessPolicyDAO;
+import org.apache.nifi.web.dao.ConnectionDAO;
+import org.apache.nifi.web.dao.ControllerServiceDAO;
+import org.apache.nifi.web.dao.FunnelDAO;
+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;
+import org.apache.nifi.web.dao.TemplateDAO;
+import org.apache.nifi.web.dao.UserDAO;
+import org.apache.nifi.web.dao.UserGroupDAO;
+import org.apache.nifi.web.revision.DeleteRevisionTask;
+import org.apache.nifi.web.revision.ExpiredRevisionClaimException;
+import org.apache.nifi.web.revision.RevisionClaim;
+import org.apache.nifi.web.revision.RevisionManager;
+import org.apache.nifi.web.revision.RevisionUpdate;
+import org.apache.nifi.web.revision.StandardRevisionClaim;
+import org.apache.nifi.web.revision.StandardRevisionUpdate;
+import org.apache.nifi.web.revision.UpdateRevisionTask;
+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.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Implementation of NiFiServiceFacade that performs revision checking.
+ */
+public class StandardNiFiServiceFacade implements NiFiServiceFacade {
+    private static final Logger logger = LoggerFactory.getLogger(StandardNiFiServiceFacade.class);
+    private static final int VALIDATION_WAIT_MILLIS = 50;
+
+    // nifi core components
+    private ControllerFacade controllerFacade;
+    private SnippetUtils snippetUtils;
+
+    // revision manager
+    private RevisionManager revisionManager;
+    private BulletinRepository bulletinRepository;
+
+    // data access objects
+    private ProcessorDAO processorDAO;
+    private ProcessGroupDAO processGroupDAO;
+    private RemoteProcessGroupDAO remoteProcessGroupDAO;
+    private LabelDAO labelDAO;
+    private FunnelDAO funnelDAO;
+    private SnippetDAO snippetDAO;
+    private PortDAO inputPortDAO;
+    private PortDAO outputPortDAO;
+    private ConnectionDAO connectionDAO;
+    private ControllerServiceDAO controllerServiceDAO;
+    private ReportingTaskDAO reportingTaskDAO;
+    private TemplateDAO templateDAO;
+    private UserDAO userDAO;
+    private UserGroupDAO userGroupDAO;
+    private AccessPolicyDAO accessPolicyDAO;
+    private RegistryDAO registryDAO;
+    private ClusterCoordinator clusterCoordinator;
+    private HeartbeatMonitor heartbeatMonitor;
+    private LeaderElectionManager leaderElectionManager;
+
+    // administrative services
+    private AuditService auditService;
+
+    // flow registry
+    private FlowRegistryClient flowRegistryClient;
+
+    // properties
+    private NiFiProperties properties;
+    private DtoFactory dtoFactory;
+    private EntityFactory entityFactory;
+
+    private Authorizer authorizer;
+
+    private AuthorizableLookup authorizableLookup;
+
+    // -----------------------------------------
+    // Synchronization methods
+    // -----------------------------------------
+    @Override
+    public void authorizeAccess(final AuthorizeAccess authorizeAccess) {
+        authorizeAccess.authorize(authorizableLookup);
+    }
+
+    @Override
+    public void verifyRevision(final Revision revision, final NiFiUser user) {
+        final Revision curRevision = revisionManager.getRevision(revision.getComponentId());
+        if (revision.equals(curRevision)) {
+            return;
+        }
+
+        throw new InvalidRevisionException(revision + " is not the most up-to-date revision. This component appears to have been modified");
+    }
+
+    @Override
+    public void verifyRevisions(final Set<Revision> revisions, final NiFiUser user) {
+        for (final Revision revision : revisions) {
+            verifyRevision(revision, user);
+        }
+    }
+
+    @Override
+    public Set<Revision> getRevisionsFromGroup(final String groupId, final Function<ProcessGroup, Set<String>> getComponents) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        final Set<String> componentIds = getComponents.apply(group);
+        return componentIds.stream().map(id -> revisionManager.getRevision(id)).collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Revision> getRevisionsFromSnippet(final String snippetId) {
+        final Snippet snippet = snippetDAO.getSnippet(snippetId);
+        final Set<String> componentIds = new HashSet<>();
+        componentIds.addAll(snippet.getProcessors().keySet());
+        componentIds.addAll(snippet.getFunnels().keySet());
+        componentIds.addAll(snippet.getLabels().keySet());
+        componentIds.addAll(snippet.getConnections().keySet());
+        componentIds.addAll(snippet.getInputPorts().keySet());
+        componentIds.addAll(snippet.getOutputPorts().keySet());
+        componentIds.addAll(snippet.getProcessGroups().keySet());
+        componentIds.addAll(snippet.getRemoteProcessGroups().keySet());
+        return componentIds.stream().map(id -> revisionManager.getRevision(id)).collect(Collectors.toSet());
+    }
+
+    // -----------------------------------------
+    // Verification Operations
+    // -----------------------------------------
+
+    @Override
+    public void verifyListQueue(final String connectionId) {
+        connectionDAO.verifyList(connectionId);
+    }
+
+    @Override
+    public void verifyCreateConnection(final String groupId, final ConnectionDTO connectionDTO) {
+        connectionDAO.verifyCreate(groupId, connectionDTO);
+    }
+
+    @Override
+    public void verifyUpdateConnection(final ConnectionDTO connectionDTO) {
+        // if connection does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (connectionDAO.hasConnection(connectionDTO.getId())) {
+            connectionDAO.verifyUpdate(connectionDTO);
+        } else {
+            connectionDAO.verifyCreate(connectionDTO.getParentGroupId(), connectionDTO);
+        }
+    }
+
+    @Override
+    public void verifyDeleteConnection(final String connectionId) {
+        connectionDAO.verifyDelete(connectionId);
+    }
+
+    @Override
+    public void verifyDeleteFunnel(final String funnelId) {
+        funnelDAO.verifyDelete(funnelId);
+    }
+
+    @Override
+    public void verifyUpdateInputPort(final PortDTO inputPortDTO) {
+        // if connection does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (inputPortDAO.hasPort(inputPortDTO.getId())) {
+            inputPortDAO.verifyUpdate(inputPortDTO);
+        }
+    }
+
+    @Override
+    public void verifyDeleteInputPort(final String inputPortId) {
+        inputPortDAO.verifyDelete(inputPortId);
+    }
+
+    @Override
+    public void verifyUpdateOutputPort(final PortDTO outputPortDTO) {
+        // if connection does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (outputPortDAO.hasPort(outputPortDTO.getId())) {
+            outputPortDAO.verifyUpdate(outputPortDTO);
+        }
+    }
+
+    @Override
+    public void verifyDeleteOutputPort(final String outputPortId) {
+        outputPortDAO.verifyDelete(outputPortId);
+    }
+
+    @Override
+    public void verifyCreateProcessor(ProcessorDTO processorDTO) {
+        processorDAO.verifyCreate(processorDTO);
+    }
+
+    @Override
+    public void verifyUpdateProcessor(final ProcessorDTO processorDTO) {
+        // if group does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (processorDAO.hasProcessor(processorDTO.getId())) {
+            processorDAO.verifyUpdate(processorDTO);
+        } else {
+            verifyCreateProcessor(processorDTO);
+        }
+    }
+
+    @Override
+    public void verifyDeleteProcessor(final String processorId) {
+        processorDAO.verifyDelete(processorId);
+    }
+
+    @Override
+    public void verifyScheduleComponents(final String groupId, final ScheduledState state, final Set<String> componentIds) {
+        processGroupDAO.verifyScheduleComponents(groupId, state, componentIds);
+    }
+
+    @Override
+    public void verifyEnableComponents(String processGroupId, ScheduledState state, Set<String> componentIds) {
+        processGroupDAO.verifyEnableComponents(processGroupId, state, componentIds);
+    }
+
+    @Override
+    public void verifyActivateControllerServices(final String groupId, final ControllerServiceState state, final Collection<String> serviceIds) {
+        processGroupDAO.verifyActivateControllerServices(state, serviceIds);
+    }
+
+    @Override
+    public void verifyDeleteProcessGroup(final String groupId) {
+        processGroupDAO.verifyDelete(groupId);
+    }
+
+    @Override
+    public void verifyUpdateRemoteProcessGroup(final RemoteProcessGroupDTO remoteProcessGroupDTO) {
+        // if remote group does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (remoteProcessGroupDAO.hasRemoteProcessGroup(remoteProcessGroupDTO.getId())) {
+            remoteProcessGroupDAO.verifyUpdate(remoteProcessGroupDTO);
+        }
+    }
+
+    @Override
+    public void verifyUpdateRemoteProcessGroupInputPort(final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) {
+        remoteProcessGroupDAO.verifyUpdateInputPort(remoteProcessGroupId, remoteProcessGroupPortDTO);
+    }
+
+    @Override
+    public void verifyUpdateRemoteProcessGroupOutputPort(final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) {
+        remoteProcessGroupDAO.verifyUpdateOutputPort(remoteProcessGroupId, remoteProcessGroupPortDTO);
+    }
+
+    @Override
+    public void verifyDeleteRemoteProcessGroup(final String remoteProcessGroupId) {
+        remoteProcessGroupDAO.verifyDelete(remoteProcessGroupId);
+    }
+
+    @Override
+    public void verifyCreateControllerService(ControllerServiceDTO controllerServiceDTO) {
+        controllerServiceDAO.verifyCreate(controllerServiceDTO);
+    }
+
+    @Override
+    public void verifyUpdateControllerService(final ControllerServiceDTO controllerServiceDTO) {
+        // if service does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (controllerServiceDAO.hasControllerService(controllerServiceDTO.getId())) {
+            controllerServiceDAO.verifyUpdate(controllerServiceDTO);
+        } else {
+            verifyCreateControllerService(controllerServiceDTO);
+        }
+    }
+
+    @Override
+    public void verifyUpdateControllerServiceReferencingComponents(final String controllerServiceId, final ScheduledState scheduledState, final ControllerServiceState controllerServiceState) {
+        controllerServiceDAO.verifyUpdateReferencingComponents(controllerServiceId, scheduledState, controllerServiceState);
+    }
+
+    @Override
+    public void verifyDeleteControllerService(final String controllerServiceId) {
+        controllerServiceDAO.verifyDelete(controllerServiceId);
+    }
+
+    @Override
+    public void verifyCreateReportingTask(ReportingTaskDTO reportingTaskDTO) {
+        reportingTaskDAO.verifyCreate(reportingTaskDTO);
+    }
+
+    @Override
+    public void verifyUpdateReportingTask(final ReportingTaskDTO reportingTaskDTO) {
+        // if tasks does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (reportingTaskDAO.hasReportingTask(reportingTaskDTO.getId())) {
+            reportingTaskDAO.verifyUpdate(reportingTaskDTO);
+        } else {
+            verifyCreateReportingTask(reportingTaskDTO);
+        }
+    }
+
+    @Override
+    public void verifyDeleteReportingTask(final String reportingTaskId) {
+        reportingTaskDAO.verifyDelete(reportingTaskId);
+    }
+
+    // -----------------------------------------
+    // Write Operations
+    // -----------------------------------------
+
+    @Override
+    public AccessPolicyEntity updateAccessPolicy(final Revision revision, final AccessPolicyDTO accessPolicyDTO) {
+        final Authorizable authorizable = authorizableLookup.getAccessPolicyById(accessPolicyDTO.getId());
+        final RevisionUpdate<AccessPolicyDTO> snapshot = updateComponent(revision,
+                authorizable,
+                () -> accessPolicyDAO.updateAccessPolicy(accessPolicyDTO),
+                accessPolicy -> {
+                    final Set<TenantEntity> users = accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet());
+                    final Set<TenantEntity> userGroups = accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet());
+                    final ComponentReferenceEntity componentReference = createComponentReferenceEntity(accessPolicy.getResource());
+                    return dtoFactory.createAccessPolicyDto(accessPolicy, userGroups, users, componentReference);
+                });
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizable);
+        return entityFactory.createAccessPolicyEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+    @Override
+    public UserEntity updateUser(final Revision revision, final UserDTO userDTO) {
+        final Authorizable usersAuthorizable = authorizableLookup.getTenant();
+        final Set<Group> groups = userGroupDAO.getUserGroupsForUser(userDTO.getId());
+        final Set<AccessPolicy> policies = userGroupDAO.getAccessPoliciesForUser(userDTO.getId());
+        final RevisionUpdate<UserDTO> snapshot = updateComponent(revision,
+                usersAuthorizable,
+                () -> userDAO.updateUser(userDTO),
+                user -> {
+                    final Set<TenantEntity> tenantEntities = groups.stream().map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet());
+                    final Set<AccessPolicySummaryEntity> policyEntities = policies.stream().map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+                    return dtoFactory.createUserDto(user, tenantEntities, policyEntities);
+                });
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(usersAuthorizable);
+        return entityFactory.createUserEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+    @Override
+    public UserGroupEntity updateUserGroup(final Revision revision, final UserGroupDTO userGroupDTO) {
+        final Authorizable userGroupsAuthorizable = authorizableLookup.getTenant();
+        final Set<AccessPolicy> policies = userGroupDAO.getAccessPoliciesForUserGroup(userGroupDTO.getId());
+        final RevisionUpdate<UserGroupDTO> snapshot = updateComponent(revision,
+                userGroupsAuthorizable,
+                () -> userGroupDAO.updateUserGroup(userGroupDTO),
+                userGroup -> {
+                    final Set<TenantEntity> tenantEntities = userGroup.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet());
+                    final Set<AccessPolicySummaryEntity> policyEntities = policies.stream().map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+                    return dtoFactory.createUserGroupDto(userGroup, tenantEntities, policyEntities);
+                }
+        );
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(userGroupsAuthorizable);
+        return entityFactory.createUserGroupEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+    @Override
+    public ConnectionEntity updateConnection(final Revision revision, final ConnectionDTO connectionDTO) {
+        final Connection connectionNode = connectionDAO.getConnection(connectionDTO.getId());
+
+        final RevisionUpdate<ConnectionDTO> snapshot = updateComponent(
+                revision,
+                connectionNode,
+                () -> connectionDAO.updateConnection(connectionDTO),
+                connection -> dtoFactory.createConnectionDto(connection));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connectionNode);
+        final ConnectionStatusDTO status = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connectionNode.getIdentifier()));
+        return entityFactory.createConnectionEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, status);
+    }
+
+    @Override
+    public ProcessorEntity updateProcessor(final Revision revision, final ProcessorDTO processorDTO) {
+        // get the component, ensure we have access to it, and perform the update request
+        final ProcessorNode processorNode = processorDAO.getProcessor(processorDTO.getId());
+        final RevisionUpdate<ProcessorDTO> snapshot = updateComponent(revision,
+                processorNode,
+                () -> processorDAO.updateProcessor(processorDTO),
+                proc -> {
+                    awaitValidationCompletion(proc);
+                    return dtoFactory.createProcessorDto(proc);
+                });
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processorNode);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processorNode));
+        final ProcessorStatusDTO status = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processorNode.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processorNode.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createProcessorEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    private void awaitValidationCompletion(final ComponentNode component) {
+        component.getValidationStatus(VALIDATION_WAIT_MILLIS, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public LabelEntity updateLabel(final Revision revision, final LabelDTO labelDTO) {
+        final Label labelNode = labelDAO.getLabel(labelDTO.getId());
+        final RevisionUpdate<LabelDTO> snapshot = updateComponent(revision,
+                labelNode,
+                () -> labelDAO.updateLabel(labelDTO),
+                label -> dtoFactory.createLabelDto(label));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(labelNode);
+        return entityFactory.createLabelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+    @Override
+    public FunnelEntity updateFunnel(final Revision revision, final FunnelDTO funnelDTO) {
+        final Funnel funnelNode = funnelDAO.getFunnel(funnelDTO.getId());
+        final RevisionUpdate<FunnelDTO> snapshot = updateComponent(revision,
+                funnelNode,
+                () -> funnelDAO.updateFunnel(funnelDTO),
+                funnel -> dtoFactory.createFunnelDto(funnel));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnelNode);
+        return entityFactory.createFunnelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+
+    /**
+     * Updates a component with the given revision, using the provided supplier to call
+     * into the appropriate DAO and the provided function to convert the component into a DTO.
+     *
+     * @param revision    the current revision
+     * @param daoUpdate   a Supplier that will update the component via the appropriate DAO
+     * @param dtoCreation a Function to convert a component into a dao
+     * @param <D>         the DTO Type of the updated component
+     * @param <C>         the Component Type of the updated component
+     * @return A RevisionUpdate that represents the new configuration
+     */
+    private <D, C> RevisionUpdate<D> updateComponent(final Revision revision, final Authorizable authorizable, final Supplier<C> daoUpdate, final Function<C, D> dtoCreation) {
+        try {
+            final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+            final RevisionUpdate<D> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(revision), user, new UpdateRevisionTask<D>() {
+                @Override
+                public RevisionUpdate<D> update() {
+                    // get the updated component
+                    final C component = daoUpdate.get();
+
+                    // save updated controller
+                    controllerFacade.save();
+
+                    final D dto = dtoCreation.apply(component);
+
+                    final Revision updatedRevision = revisionManager.getRevision(revision.getComponentId()).incrementRevision(revision.getClientId());
+                    final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity());
+                    return new StandardRevisionUpdate<>(dto, lastModification);
+                }
+            });
+
+            return updatedComponent;
+        } catch (final ExpiredRevisionClaimException erce) {
+            throw new InvalidRevisionException("Failed to update component " + authorizable, erce);
+        }
+    }
+
+
+    @Override
+    public void verifyUpdateSnippet(final SnippetDTO snippetDto, final Set<String> affectedComponentIds) {
+        // if snippet does not exist, then the update request is likely creating it
+        // so we don't verify since it will fail
+        if (snippetDAO.hasSnippet(snippetDto.getId())) {
+            snippetDAO.verifyUpdateSnippetComponent(snippetDto);
+        }
+    }
+
+    @Override
+    public SnippetEntity updateSnippet(final Set<Revision> revisions, final SnippetDTO snippetDto) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revisions);
+
+        final RevisionUpdate<SnippetDTO> snapshot;
+        try {
+            snapshot = revisionManager.updateRevision(revisionClaim, user, new UpdateRevisionTask<SnippetDTO>() {
+                @Override
+                public RevisionUpdate<SnippetDTO> update() {
+                    // get the updated component
+                    final Snippet snippet = snippetDAO.updateSnippetComponents(snippetDto);
+
+                    // drop the snippet
+                    snippetDAO.dropSnippet(snippet.getId());
+
+                    // save updated controller
+                    controllerFacade.save();
+
+                    // increment the revisions
+                    final Set<Revision> updatedRevisions = revisions.stream().map(revision -> {
+                        final Revision currentRevision = revisionManager.getRevision(revision.getComponentId());
+                        return currentRevision.incrementRevision(revision.getClientId());
+                    }).collect(Collectors.toSet());
+
+                    final SnippetDTO dto = dtoFactory.createSnippetDto(snippet);
+                    return new StandardRevisionUpdate<>(dto, null, updatedRevisions);
+                }
+            });
+        } catch (final ExpiredRevisionClaimException e) {
+            throw new InvalidRevisionException("Failed to update Snippet", e);
+        }
+
+        return entityFactory.createSnippetEntity(snapshot.getComponent());
+    }
+
+    @Override
+    public PortEntity updateInputPort(final Revision revision, final PortDTO inputPortDTO) {
+        final Port inputPortNode = inputPortDAO.getPort(inputPortDTO.getId());
+        final RevisionUpdate<PortDTO> snapshot = updateComponent(revision,
+                inputPortNode,
+                () -> inputPortDAO.updatePort(inputPortDTO),
+                port -> dtoFactory.createPortDto(port));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(inputPortNode);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(inputPortNode));
+        final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(inputPortNode.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPortNode.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public PortEntity updateOutputPort(final Revision revision, final PortDTO outputPortDTO) {
+        final Port outputPortNode = outputPortDAO.getPort(outputPortDTO.getId());
+        final RevisionUpdate<PortDTO> snapshot = updateComponent(revision,
+                outputPortNode,
+                () -> outputPortDAO.updatePort(outputPortDTO),
+                port -> dtoFactory.createPortDto(port));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(outputPortNode);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(outputPortNode), NiFiUserUtils.getNiFiUser());
+        final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(outputPortNode.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPortNode.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public RemoteProcessGroupEntity updateRemoteProcessGroup(final Revision revision, final RemoteProcessGroupDTO remoteProcessGroupDTO) {
+        final RemoteProcessGroup remoteProcessGroupNode = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupDTO.getId());
+        final RevisionUpdate<RemoteProcessGroupDTO> snapshot = updateComponent(
+                revision,
+                remoteProcessGroupNode,
+                () -> remoteProcessGroupDAO.updateRemoteProcessGroup(remoteProcessGroupDTO),
+                remoteProcessGroup -> dtoFactory.createRemoteProcessGroupDto(remoteProcessGroup));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroupNode);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroupNode));
+        final RevisionDTO updateRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
+        final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(remoteProcessGroupNode,
+                controllerFacade.getRemoteProcessGroupStatus(remoteProcessGroupNode.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroupNode.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createRemoteProcessGroupEntity(snapshot.getComponent(), updateRevision, permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public RemoteProcessGroupPortEntity updateRemoteProcessGroupInputPort(
+            final Revision revision, final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) {
+
+        final RemoteProcessGroup remoteProcessGroupNode = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupPortDTO.getGroupId());
+        final RevisionUpdate<RemoteProcessGroupPortDTO> snapshot = updateComponent(
+                revision,
+                remoteProcessGroupNode,
+                () -> remoteProcessGroupDAO.updateRemoteProcessGroupInputPort(remoteProcessGroupId, remoteProcessGroupPortDTO),
+                remoteGroupPort -> dtoFactory.createRemoteProcessGroupPortDto(remoteGroupPort));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroupNode);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroupNode));
+        final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
+        return entityFactory.createRemoteProcessGroupPortEntity(snapshot.getComponent(), updatedRevision, permissions, operatePermissions);
+    }
+
+    @Override
+    public RemoteProcessGroupPortEntity updateRemoteProcessGroupOutputPort(
+            final Revision revision, final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) {
+
+        final RemoteProcessGroup remoteProcessGroupNode = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupPortDTO.getGroupId());
+        final RevisionUpdate<RemoteProcessGroupPortDTO> snapshot = updateComponent(
+                revision,
+                remoteProcessGroupNode,
+                () -> remoteProcessGroupDAO.updateRemoteProcessGroupOutputPort(remoteProcessGroupId, remoteProcessGroupPortDTO),
+                remoteGroupPort -> dtoFactory.createRemoteProcessGroupPortDto(remoteGroupPort));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroupNode);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroupNode));
+        final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
+        return entityFactory.createRemoteProcessGroupPortEntity(snapshot.getComponent(), updatedRevision, permissions, operatePermissions);
+    }
+
+    @Override
+    public Set<AffectedComponentDTO> getActiveComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId());
+        if (group == null) {
+            throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistryDto.getProcessGroupId());
+        }
+
+        final Map<String, String> variableMap = new HashMap<>();
+        variableRegistryDto.getVariables().stream() // have to use forEach here instead of using Collectors.toMap because value may be null
+            .map(VariableEntity::getVariable)
+            .forEach(var -> variableMap.put(var.getName(), var.getValue()));
+
+        final Set<AffectedComponentDTO> affectedComponentDtos = new HashSet<>();
+
+        final Set<String> updatedVariableNames = getUpdatedVariables(group, variableMap);
+        for (final String variableName : updatedVariableNames) {
+            final Set<ComponentNode> affectedComponents = group.getComponentsAffectedByVariable(variableName);
+
+            for (final ComponentNode component : affectedComponents) {
+                if (component instanceof ProcessorNode) {
+                    final ProcessorNode procNode = (ProcessorNode) component;
+                    if (procNode.isRunning()) {
+                        affectedComponentDtos.add(dtoFactory.createAffectedComponentDto(procNode));
+                    }
+                } else if (component instanceof ControllerServiceNode) {
+                    final ControllerServiceNode serviceNode = (ControllerServiceNode) component;
+                    if (serviceNode.isActive()) {
+                        affectedComponentDtos.add(dtoFactory.createAffectedComponentDto(serviceNode));
+                    }
+                } else {
+                    throw new RuntimeException("Found unexpected type of Component [" + component.getCanonicalClassName() + "] dependending on variable");
+                }
+            }
+        }
+
+        return affectedComponentDtos;
+    }
+
+    @Override
+    public Set<AffectedComponentEntity> getComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId());
+        if (group == null) {
+            throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistryDto.getProcessGroupId());
+        }
+
+        final Map<String, String> variableMap = new HashMap<>();
+        variableRegistryDto.getVariables().stream() // have to use forEach here instead of using Collectors.toMap because value may be null
+                .map(VariableEntity::getVariable)
+                .forEach(var -> variableMap.put(var.getName(), var.getValue()));
+
+        final Set<AffectedComponentEntity> affectedComponentEntities = new HashSet<>();
+
+        final Set<String> updatedVariableNames = getUpdatedVariables(group, variableMap);
+        for (final String variableName : updatedVariableNames) {
+            final Set<ComponentNode> affectedComponents = group.getComponentsAffectedByVariable(variableName);
+            affectedComponentEntities.addAll(dtoFactory.createAffectedComponentEntities(affectedComponents, revisionManager));
+        }
+
+        return affectedComponentEntities;
+    }
+
+    private Set<String> getUpdatedVariables(final ProcessGroup group, final Map<String, String> newVariableValues) {
+        final Set<String> updatedVariableNames = new HashSet<>();
+
+        final ComponentVariableRegistry registry = group.getVariableRegistry();
+        for (final Map.Entry<String, String> entry : newVariableValues.entrySet()) {
+            final String varName = entry.getKey();
+            final String newValue = entry.getValue();
+
+            final String curValue = registry.getVariableValue(varName);
+            if (!Objects.equals(newValue, curValue)) {
+                updatedVariableNames.add(varName);
+            }
+        }
+
+        return updatedVariableNames;
+    }
+
+
+    @Override
+    public VariableRegistryEntity updateVariableRegistry(Revision revision, VariableRegistryDTO variableRegistryDto) {
+        final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId());
+        final RevisionUpdate<VariableRegistryDTO> snapshot = updateComponent(revision,
+            processGroupNode,
+            () -> processGroupDAO.updateVariableRegistry(variableRegistryDto),
+            processGroup -> dtoFactory.createVariableRegistryDto(processGroup, revisionManager));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);
+        final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
+        return entityFactory.createVariableRegistryEntity(snapshot.getComponent(), updatedRevision, permissions);
+    }
+
+
+    @Override
+    public ProcessGroupEntity updateProcessGroup(final Revision revision, final ProcessGroupDTO processGroupDTO) {
+        final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(processGroupDTO.getId());
+        final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(revision,
+                processGroupNode,
+                () -> processGroupDAO.updateProcessGroup(processGroupDTO),
+                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 verifyUpdateProcessGroup(ProcessGroupDTO processGroupDTO) {
+        if (processGroupDAO.hasProcessGroup(processGroupDTO.getId())) {
+            processGroupDAO.verifyUpdate(processGroupDTO);
+        }
+    }
+
+    @Override
+    public ScheduleComponentsEntity enableComponents(String processGroupId, ScheduledState state, Map<String, Revision> componentRevisions) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        final RevisionUpdate<ScheduleComponentsEntity> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(componentRevisions.values()), user, new
+                UpdateRevisionTask<ScheduleComponentsEntity>() {
+                    @Override
+                    public RevisionUpdate<ScheduleComponentsEntity> update() {
+                        // schedule the components
+                        processGroupDAO.enableComponents(processGroupId, state, componentRevisions.keySet());
+
+                        // update the revisions
+                        final Map<String, Revision> updatedRevisions = new HashMap<>();
+                        for (final Revision revision : componentRevisions.values()) {
+                            final Revision currentRevision = revisionManager.getRevision(revision.getComponentId());
+                            updatedRevisions.put(revision.getComponentId(), currentRevision.incrementRevision(revision.getClientId()));
+                        }
+
+                        // save
+                        controllerFacade.save();
+
+                        // gather details for response
+                        final ScheduleComponentsEntity entity = new ScheduleComponentsEntity();
+                        entity.setId(processGroupId);
+                        entity.setState(state.name());
+                        return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values()));
+                    }
+                });
+
+        return updatedComponent.getComponent();
+    }
+
+    @Override
+    public ScheduleComponentsEntity scheduleComponents(final String processGroupId, final ScheduledState state, final Map<String, Revision> componentRevisions) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final RevisionUpdate<ScheduleComponentsEntity> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(componentRevisions.values()), user, new
+                UpdateRevisionTask<ScheduleComponentsEntity>() {
+                    @Override
+                    public RevisionUpdate<ScheduleComponentsEntity> update() {
+                        // schedule the components
+                        processGroupDAO.scheduleComponents(processGroupId, state, componentRevisions.keySet());
+
+                        // update the revisions
+                        final Map<String, Revision> updatedRevisions = new HashMap<>();
+                        for (final Revision revision : componentRevisions.values()) {
+                            final Revision currentRevision = revisionManager.getRevision(revision.getComponentId());
+                            updatedRevisions.put(revision.getComponentId(), currentRevision.incrementRevision(revision.getClientId()));
+                        }
+
+                        // save
+                        controllerFacade.save();
+
+                        // gather details for response
+                        final ScheduleComponentsEntity entity = new ScheduleComponentsEntity();
+                        entity.setId(processGroupId);
+                        entity.setState(state.name());
+                        return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values()));
+                    }
+                });
+
+        return updatedComponent.getComponent();
+    }
+
+    @Override
+    public ActivateControllerServicesEntity activateControllerServices(final String processGroupId, final ControllerServiceState state, final Map<String, Revision> serviceRevisions) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final RevisionUpdate<ActivateControllerServicesEntity> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(serviceRevisions.values()), user,
+            new UpdateRevisionTask<ActivateControllerServicesEntity>() {
+                @Override
+                public RevisionUpdate<ActivateControllerServicesEntity> update() {
+                    // schedule the components
+                    processGroupDAO.activateControllerServices(processGroupId, state, serviceRevisions.keySet());
+
+                    // update the revisions
+                    final Map<String, Revision> updatedRevisions = new HashMap<>();
+                    for (final Revision revision : serviceRevisions.values()) {
+                        final Revision currentRevision = revisionManager.getRevision(revision.getComponentId());
+                        updatedRevisions.put(revision.getComponentId(), currentRevision.incrementRevision(revision.getClientId()));
+                    }
+
+                    // save
+                    controllerFacade.save();
+
+                    // gather details for response
+                    final ActivateControllerServicesEntity entity = new ActivateControllerServicesEntity();
+                    entity.setId(processGroupId);
+                    entity.setState(state.name());
+                    return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values()));
+                }
+            });
+
+        return updatedComponent.getComponent();
+    }
+
+
+    @Override
+    public ControllerConfigurationEntity updateControllerConfiguration(final Revision revision, final ControllerConfigurationDTO controllerConfigurationDTO) {
+        final RevisionUpdate<ControllerConfigurationDTO> updatedComponent = updateComponent(
+                revision,
+                controllerFacade,
+                () -> {
+                    if (controllerConfigurationDTO.getMaxTimerDrivenThreadCount() != null) {
+                        controllerFacade.setMaxTimerDrivenThreadCount(controllerConfigurationDTO.getMaxTimerDrivenThreadCount());
+                    }
+                    if (controllerConfigurationDTO.getMaxEventDrivenThreadCount() != null) {
+                        controllerFacade.setMaxEventDrivenThreadCount(controllerConfigurationDTO.getMaxEventDrivenThreadCount());
+                    }
+
+                    return controllerConfigurationDTO;
+                },
+                controller -> dtoFactory.createControllerConfigurationDto(controllerFacade));
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerFacade);
+        final RevisionDTO updateRevision = dtoFactory.createRevisionDTO(updatedComponent.getLastModification());
+        return entityFactory.createControllerConfigurationEntity(updatedComponent.getComponent(), updateRevision, permissions);
+    }
+
+
+    @Override
+    public NodeDTO updateNode(final NodeDTO nodeDTO) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new WebApplicationException(new Throwable("Unable to access details for current user."));
+        }
+        final String userDn = user.getIdentity();
+
+        final NodeIdentifier nodeId = clusterCoordinator.getNodeIdentifier(nodeDTO.getNodeId());
+        if (nodeId == null) {
+            throw new UnknownNodeException("No node exists with ID " + nodeDTO.getNodeId());
+        }
+
+
+        if (NodeConnectionState.CONNECTING.name().equalsIgnoreCase(nodeDTO.getStatus())) {
+            clusterCoordinator.requestNodeConnect(nodeId, userDn);
+        } else if (NodeConnectionState.OFFLOADING.name().equalsIgnoreCase(nodeDTO.getStatus())) {
+            clusterCoordinator.requestNodeOffload(nodeId, OffloadCode.OFFLOADED,
+                    "User " + userDn + " requested that node be offloaded");
+        } else if (NodeConnectionState.DISCONNECTING.name().equalsIgnoreCase(nodeDTO.getStatus())) {
+            clusterCoordinator.requestNodeDisconnect(nodeId, DisconnectionCode.USER_DISCONNECTED,
+                    "User " + userDn + " requested that node be disconnected from cluster");
+        }
+
+        return getNode(nodeId);
+    }
+
+    @Override
+    public CounterDTO updateCounter(final String counterId) {
+        return dtoFactory.createCounterDto(controllerFacade.resetCounter(counterId));
+    }
+
+    @Override
+    public void verifyCanClearProcessorState(final String processorId) {
+        processorDAO.verifyClearState(processorId);
+    }
+
+    @Override
+    public void clearProcessorState(final String processorId) {
+        processorDAO.clearState(processorId);
+    }
+
+    @Override
+    public void verifyCanClearControllerServiceState(final String controllerServiceId) {
+        controllerServiceDAO.verifyClearState(controllerServiceId);
+    }
+
+    @Override
+    public void clearControllerServiceState(final String controllerServiceId) {
+        controllerServiceDAO.clearState(controllerServiceId);
+    }
+
+    @Override
+    public void verifyCanClearReportingTaskState(final String reportingTaskId) {
+        reportingTaskDAO.verifyClearState(reportingTaskId);
+    }
+
+    @Override
+    public void clearReportingTaskState(final String reportingTaskId) {
+        reportingTaskDAO.clearState(reportingTaskId);
+    }
+
+    @Override
+    public ConnectionEntity deleteConnection(final Revision revision, final String connectionId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection);
+        final ConnectionDTO snapshot = deleteComponent(
+                revision,
+                connection.getResource(),
+                () -> connectionDAO.deleteConnection(connectionId),
+                false, // no policies to remove
+                dtoFactory.createConnectionDto(connection));
+
+        return entityFactory.createConnectionEntity(snapshot, null, permissions, null);
+    }
+
+    @Override
+    public DropRequestDTO deleteFlowFileDropRequest(final String connectionId, final String dropRequestId) {
+        return dtoFactory.createDropRequestDTO(connectionDAO.deleteFlowFileDropRequest(connectionId, dropRequestId));
+    }
+
+    @Override
+    public ListingRequestDTO deleteFlowFileListingRequest(final String connectionId, final String listingRequestId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        final ListingRequestDTO listRequest = dtoFactory.createListingRequestDTO(connectionDAO.deleteFlowFileListingRequest(connectionId, listingRequestId));
+
+        // include whether the source and destination are running
+        if (connection.getSource() != null) {
+            listRequest.setSourceRunning(connection.getSource().isRunning());
+        }
+        if (connection.getDestination() != null) {
+            listRequest.setDestinationRunning(connection.getDestination().isRunning());
+        }
+
+        return listRequest;
+    }
+
+    @Override
+    public ProcessorEntity deleteProcessor(final Revision revision, final String processorId) {
+        final ProcessorNode processor = processorDAO.getProcessor(processorId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processor));
+        final ProcessorDTO snapshot = deleteComponent(
+                revision,
+                processor.getResource(),
+                () -> processorDAO.deleteProcessor(processorId),
+                true,
+                dtoFactory.createProcessorDto(processor));
+
+        return entityFactory.createProcessorEntity(snapshot, null, permissions, operatePermissions, null, null);
+    }
+
+    @Override
+    public ProcessorEntity terminateProcessor(final String processorId) {
+        processorDAO.terminate(processorId);
+        return getProcessor(processorId);
+    }
+
+    @Override
+    public void verifyTerminateProcessor(final String processorId) {
+        processorDAO.verifyTerminate(processorId);
+    }
+
+    @Override
+    public LabelEntity deleteLabel(final Revision revision, final String labelId) {
+        final Label label = labelDAO.getLabel(labelId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(label);
+        final LabelDTO snapshot = deleteComponent(
+                revision,
+                label.getResource(),
+                () -> labelDAO.deleteLabel(labelId),
+                true,
+                dtoFactory.createLabelDto(label));
+
+        return entityFactory.createLabelEntity(snapshot, null, permissions);
+    }
+
+    @Override
+    public UserEntity deleteUser(final Revision revision, final String userId) {
+        final User user = userDAO.getUser(userId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant());
+        final Set<TenantEntity> userGroups = user != null ? userGroupDAO.getUserGroupsForUser(userId).stream()
+                .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()) : null;
+        final Set<AccessPolicySummaryEntity> policyEntities = user != null ? userGroupDAO.getAccessPoliciesForUser(userId).stream()
+                .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()) : null;
+
+        final String resourceIdentifier = ResourceFactory.getTenantResource().getIdentifier() + "/" + userId;
+        final UserDTO snapshot = deleteComponent(
+                revision,
+                new Resource() {
+                    @Override
+                    public String getIdentifier() {
+                        return resourceIdentifier;
+                    }
+
+                    @Override
+                    public String getName() {
+                        return resourceIdentifier;
+                    }
+
+                    @Override
+                    public String getSafeDescription() {
+                        return "User " + userId;
+                    }
+                },
+                () -> userDAO.deleteUser(userId),
+                false, // no user specific policies to remove
+                dtoFactory.createUserDto(user, userGroups, policyEntities));
+
+        return entityFactory.createUserEntity(snapshot, null, permissions);
+    }
+
+    @Override
+    public UserGroupEntity deleteUserGroup(final Revision revision, final String userGroupId) {
+        final Group userGroup = userGroupDAO.getUserGroup(userGroupId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant());
+        final Set<TenantEntity> users = userGroup != null ? userGroup.getUsers().stream()
+                .map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()) : null;
+        final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUserGroup(userGroup.getIdentifier()).stream()
+                .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+
+        final String resourceIdentifier = ResourceFactory.getTenantResource().getIdentifier() + "/" + userGroupId;
+        final UserGroupDTO snapshot = deleteComponent(
+                revision,
+                new Resource() {
+                    @Override
+                    public String getIdentifier() {
+                        return resourceIdentifier;
+                    }
+
+                    @Override
+                    public String getName() {
+                        return resourceIdentifier;
+                    }
+
+                    @Override
+                    public String getSafeDescription() {
+                        return "User Group " + userGroupId;
+                    }
+                },
+                () -> userGroupDAO.deleteUserGroup(userGroupId),
+                false, // no user group specific policies to remove
+                dtoFactory.createUserGroupDto(userGroup, users, policyEntities));
+
+        return entityFactory.createUserGroupEntity(snapshot, null, permissions);
+    }
+
+    @Override
+    public AccessPolicyEntity deleteAccessPolicy(final Revision revision, final String accessPolicyId) {
+        final AccessPolicy accessPolicy = accessPolicyDAO.getAccessPolicy(accessPolicyId);
+        final ComponentReferenceEntity componentReference = createComponentReferenceEntity(accessPolicy.getResource());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(accessPolicyId));
+        final Set<TenantEntity> userGroups = accessPolicy != null ? accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()) : null;
+        final Set<TenantEntity> users = accessPolicy != null ? accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()) : null;
+        final AccessPolicyDTO snapshot = deleteComponent(
+                revision,
+                new Resource() {
+                    @Override
+                    public String getIdentifier() {
+                        return accessPolicy.getResource();
+                    }
+
+                    @Override
+                    public String getName() {
+                        return accessPolicy.getResource();
+                    }
+
+                    @Override
+                    public String getSafeDescription() {
+                        return "Policy " + accessPolicyId;
+                    }
+                },
+                () -> accessPolicyDAO.deleteAccessPolicy(accessPolicyId),
+                false, // no need to clean up any policies as it's already been removed above
+                dtoFactory.createAccessPolicyDto(accessPolicy, userGroups, users, componentReference));
+
+        return entityFactory.createAccessPolicyEntity(snapshot, null, permissions);
+    }
+
+    @Override
+    public FunnelEntity deleteFunnel(final Revision revision, final String funnelId) {
+        final Funnel funnel = funnelDAO.getFunnel(funnelId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnel);
+        final FunnelDTO snapshot = deleteComponent(
+                revision,
+                funnel.getResource(),
+                () -> funnelDAO.deleteFunnel(funnelId),
+                true,
+                dtoFactory.createFunnelDto(funnel));
+
+        return entityFactory.createFunnelEntity(snapshot, null, permissions);
+    }
+
+    /**
+     * Deletes a component using the Optimistic Locking Manager
+     *
+     * @param revision     the current revision
+     * @param resource the resource being removed
+     * @param deleteAction the action that deletes the component via the appropriate DAO object
+     * @param cleanUpPolicies whether or not the policies for this resource should be removed as well - not necessary when there are
+     *                        no component specific policies or if the policies of the component are inherited
+     * @return a dto that represents the new configuration
+     */
+    private <D, C> D deleteComponent(final Revision revision, final Resource resource, final Runnable deleteAction, final boolean cleanUpPolicies, final D dto) {
+        final RevisionClaim claim = new StandardRevisionClaim(revision);
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        return revisionManager.deleteRevision(claim, user, new DeleteRevisionTask<D>() {
+            @Override
+            public D performTask() {
+                logger.debug("Attempting to delete component {} with claim {}", resource.getIdentifier(), claim);
+
+                // run the delete action
+                deleteAction.run();
+
+                // save the flow
+                controllerFacade.save();
+                logger.debug("Deletion of component {} was successful", resource.getIdentifier());
+
+                if (cleanUpPolicies) {
+                    cleanUpPolicies(resource);
+                }
+
+                return dto;
+            }
+        });
+    }
+
+    /**
+     * Clean up the policies for the specified component resource.
+     *
+     * @param componentResource the resource for the component
+     */
+    private void cleanUpPolicies(final Resource componentResource) {
+        // ensure the authorizer supports configuration
+        if (accessPolicyDAO.supportsConfigurableAuthorizer()) {
+            final List<Resource> resources = new ArrayList<>();
+            resources.add(componentResource);
+            resources.add(ResourceFactory.getDataResource(componentResource));
+            resources.add(ResourceFactory.getProvenanceDataResource(componentResource));
+            resources.add(ResourceFactory.getDataTransferResource(componentResource));
+            resources.add(ResourceFactory.getPolicyResource(componentResource));
+
+            for (final Resource resource : resources) {
+                for (final RequestAction action : RequestAction.values()) {
+                    try {
+                        // since the component is being deleted, also delete any relevant access policies
+                        final AccessPolicy readPolicy = accessPolicyDAO.getAccessPolicy(action, resource.getIdentifier());
+                        if (readPolicy != null) {
+                            accessPolicyDAO.deleteAccessPolicy(readPolicy.getIdentifier());
+                        }
+                    } catch (final Exception e) {
+                        logger.warn(String.format("Unable to remove access policy for %s %s after component removal.", action, resource.getIdentifier()), e);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void verifyDeleteSnippet(final String snippetId, final Set<String> affectedComponentIds) {
+        snippetDAO.verifyDeleteSnippetComponents(snippetId);
+    }
+
+    @Override
+    public SnippetEntity deleteSnippet(final Set<Revision> revisions, final String snippetId) {
+        final Snippet snippet = snippetDAO.getSnippet(snippetId);
+
+        // grab the resources in the snippet so we can delete the policies afterwards
+        final Set<Resource> snippetResources = new HashSet<>();
+        snippet.getProcessors().keySet().forEach(id -> snippetResources.add(processorDAO.getProcessor(id).getResource()));
+        snippet.getInputPorts().keySet().forEach(id -> snippetResources.add(inputPortDAO.getPort(id).getResource()));
+        snippet.getOutputPorts().keySet().forEach(id -> snippetResources.add(outputPortDAO.getPort(id).getResource()));
+        snippet.getFunnels().keySet().forEach(id -> snippetResources.add(funnelDAO.getFunnel(id).getResource()));
+        snippet.getLabels().keySet().forEach(id -> snippetResources.add(labelDAO.getLabel(id).getResource()));
+        snippet.getRemoteProcessGroups().keySet().forEach(id -> snippetResources.add(remoteProcessGroupDAO.getRemoteProcessGroup(id).getResource()));
+        snippet.getProcessGroups().keySet().forEach(id -> {
+            final ProcessGroup processGroup = processGroupDAO.getProcessGroup(id);
+
+            // add the process group
+            snippetResources.add(processGroup.getResource());
+
+            // add each encapsulated component
+            processGroup.findAllProcessors().forEach(processor -> snippetResources.add(processor.getResource()));
+            processGroup.findAllInputPorts().forEach(inputPort -> snippetResources.add(inputPort.getResource()));
+            processGroup.findAllOutputPorts().forEach(outputPort -> snippetResources.add(outputPort.getResource()));
+            processGroup.findAllFunnels().forEach(funnel -> snippetResources.add(funnel.getResource()));
+            processGroup.findAllLabels().forEach(label -> snippetResources.add(label.getResource()));
+            processGroup.findAllProcessGroups().forEach(childGroup -> snippetResources.add(childGroup.getResource()));
+            processGroup.findAllRemoteProcessGroups().forEach(remoteProcessGroup -> snippetResources.add(remoteProcessGroup.getResource()));
+            processGroup.findAllTemplates().forEach(template -> snippetResources.add(template.getResource()));
+            processGroup.findAllControllerServices().forEach(controllerService -> snippetResources.add(controllerService.getResource()));
+        });
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final RevisionClaim claim = new StandardRevisionClaim(revisions);
+        final SnippetDTO dto = revisionManager.deleteRevision(claim, user, new DeleteRevisionTask<SnippetDTO>() {
+            @Override
+            public SnippetDTO performTask() {
+                // delete the components in the snippet
+                snippetDAO.deleteSnippetComponents(snippetId);
+
+                // drop the snippet
+                snippetDAO.dropSnippet(snippetId);
+
+                // save
+                controllerFacade.save();
+
+                // create the dto for the snippet that was just removed
+                return dtoFactory.createSnippetDto(snippet);
+            }
+        });
+
+        // clean up component policies
+        snippetResources.forEach(resource -> cleanUpPolicies(resource));
+
+        return entityFactory.createSnippetEntity(dto);
+    }
+
+    @Override
+    public PortEntity deleteInputPort(final Revision revision, final String inputPortId) {
+        final Port port = inputPortDAO.getPort(inputPortId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port));
+        final PortDTO snapshot = deleteComponent(
+                revision,
+                port.getResource(),
+                () -> inputPortDAO.deletePort(inputPortId),
+                true,
+                dtoFactory.createPortDto(port));
+
+        return entityFactory.createPortEntity(snapshot, null, permissions, operatePermissions, null, null);
+    }
+
+    @Override
+    public PortEntity deleteOutputPort(final Revision revision, final String outputPortId) {
+        final Port port = outputPortDAO.getPort(outputPortId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port));
+        final PortDTO snapshot = deleteComponent(
+                revision,
+                port.getResource(),
+                () -> outputPortDAO.deletePort(outputPortId),
+                true,
+                dtoFactory.createPortDto(port));
+
+        return entityFactory.createPortEntity(snapshot, null, permissions, operatePermissions, null, null);
+    }
+
+    @Override
+    public ProcessGroupEntity deleteProcessGroup(final Revision revision, final String groupId) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+
+        // grab the resources in the snippet so we can delete the policies afterwards
+        final Set<Resource> groupResources = new HashSet<>();
+        processGroup.findAllProcessors().forEach(processor -> groupResources.add(processor.getResource()));
+        processGroup.findAllInputPorts().forEach(inputPort -> groupResources.add(inputPort.getResource()));
+        processGroup.findAllOutputPorts().forEach(outputPort -> groupResources.add(outputPort.getResource()));
+        processGroup.findAllFunnels().forEach(funnel -> groupResources.add(funnel.getResource()));
+        processGroup.findAllLabels().forEach(label -> groupResources.add(label.getResource()));
+        processGroup.findAllProcessGroups().forEach(childGroup -> groupResources.add(childGroup.getResource()));
+        processGroup.findAllRemoteProcessGroups().forEach(remoteProcessGroup -> groupResources.add(remoteProcessGroup.getResource()));
+        processGroup.findAllTemplates().forEach(template -> groupResources.add(template.getResource()));
+        processGroup.findAllControllerServices().forEach(controllerService -> groupResources.add(controllerService.getResource()));
+
+        final ProcessGroupDTO snapshot = deleteComponent(
+                revision,
+                processGroup.getResource(),
+                () -> processGroupDAO.deleteProcessGroup(groupId),
+                true,
+                dtoFactory.createProcessGroupDto(processGroup));
+
+        // delete all applicable component policies
+        groupResources.forEach(groupResource -> cleanUpPolicies(groupResource));
+
+        return entityFactory.createProcessGroupEntity(snapshot, null, permissions, null, null);
+    }
+
+    @Override
+    public RemoteProcessGroupEntity deleteRemoteProcessGroup(final Revision revision, final String remoteProcessGroupId) {
+        final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroup));
+        final RemoteProcessGroupDTO snapshot = deleteComponent(
+                revision,
+                remoteProcessGroup.getResource(),
+                () -> remoteProcessGroupDAO.deleteRemoteProcessGroup(remoteProcessGroupId),
+                true,
+                dtoFactory.createRemoteProcessGroupDto(remoteProcessGroup));
+
+        return entityFactory.createRemoteProcessGroupEntity(snapshot, null, permissions, operatePermissions, null, null);
+    }
+
+    @Override
+    public void deleteTemplate(final String id) {
+        // delete the template and save the flow
+        templateDAO.deleteTemplate(id);
+        controllerFacade.save();
+    }
+
+    @Override
+    public ConnectionEntity createConnection(final Revision revision, final String groupId, final ConnectionDTO connectionDTO) {
+        final RevisionUpdate<ConnectionDTO> snapshot = createComponent(
+                revision,
+                connectionDTO,
+                () -> connectionDAO.createConnection(groupId, connectionDTO),
+                connection -> dtoFactory.createConnectionDto(connection));
+
+        final Connection connection = connectionDAO.getConnection(connectionDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection);
+        final ConnectionStatusDTO status = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connectionDTO.getId()));
+        return entityFactory.createConnectionEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, status);
+    }
+
+    @Override
+    public DropRequestDTO createFlowFileDropRequest(final String connectionId, final String dropRequestId) {
+        return dtoFactory.createDropRequestDTO(connectionDAO.createFlowFileDropRequest(connectionId, dropRequestId));
+    }
+
+    @Override
+    public ListingRequestDTO createFlowFileListingRequest(final String connectionId, final String listingRequestId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        final ListingRequestDTO listRequest = dtoFactory.createListingRequestDTO(connectionDAO.createFlowFileListingRequest(connectionId, listingRequestId));
+
+        // include whether the source and destination are running
+        if (connection.getSource() != null) {
+            listRequest.setSourceRunning(connection.getSource().isRunning());
+        }
+        if (connection.getDestination() != null) {
+            listRequest.setDestinationRunning(connection.getDestination().isRunning());
+        }
+
+        return listRequest;
+    }
+
+    @Override
+    public ProcessorEntity createProcessor(final Revision revision, final String groupId, final ProcessorDTO processorDTO) {
+        final RevisionUpdate<ProcessorDTO> snapshot = createComponent(
+                revision,
+                processorDTO,
+                () -> processorDAO.createProcessor(groupId, processorDTO),
+                processor -> {
+                    awaitValidationCompletion(processor);
+                    return dtoFactory.createProcessorDto(processor);
+                });
+
+        final ProcessorNode processor = processorDAO.getProcessor(processorDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processor));
+        final ProcessorStatusDTO status = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processorDTO.getId()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processorDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createProcessorEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public LabelEntity createLabel(final Revision revision, final String groupId, final LabelDTO labelDTO) {
+        final RevisionUpdate<LabelDTO> snapshot = createComponent(
+                revision,
+                labelDTO,
+                () -> labelDAO.createLabel(groupId, labelDTO),
+                label -> dtoFactory.createLabelDto(label));
+
+        final Label label = labelDAO.getLabel(labelDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(label);
+        return entityFactory.createLabelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+    /**
+     * Creates a component using the optimistic locking manager.
+     *
+     * @param componentDto the DTO that will be used to create the component
+     * @param daoCreation  A Supplier that will create the NiFi Component to use
+     * @param dtoCreation  a Function that will convert the NiFi Component into a corresponding DTO
+     * @param <D>          the DTO Type
+     * @param <C>          the NiFi Component Type
+     * @return a RevisionUpdate that represents the updated configuration
+     */
+    private <D, C> RevisionUpdate<D> createComponent(final Revision revision, final ComponentDTO componentDto, final Supplier<C> daoCreation, final Function<C, D> dtoCreation) {
+        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
+        return revisionManager.updateRevision(claim, user, () -> {
+            // add the component
+            final C component = daoCreation.get();
+
+            // save the flow
+            controllerFacade.save();
+
+            final D dto = dtoCreation.apply(component);
+            final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
+            return new StandardRevisionUpdate<>(dto, lastMod);
+        });
+    }
+
+    @Override
+    public BulletinEntity createBulletin(final BulletinDTO bulletinDTO, final Boolean canRead){
+        final Bulletin bulletin = BulletinFactory.createBulletin(bulletinDTO.getCategory(),bulletinDTO.getLevel(),bulletinDTO.getMessage());
+        bulletinRepository.addBulletin(bulletin);
+        return entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin),canRead);
+    }
+
+    @Override
+    public FunnelEntity createFunnel(final Revision revision, final String groupId, final FunnelDTO funnelDTO) {
+        final RevisionUpdate<FunnelDTO> snapshot = createComponent(
+                revision,
+                funnelDTO,
+                () -> funnelDAO.createFunnel(groupId, funnelDTO),
+                funnel -> dtoFactory.createFunnelDto(funnel));
+
+        final Funnel funnel = funnelDAO.getFunnel(funnelDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnel);
+        return entityFactory.createFunnelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions);
+    }
+
+    @Override
+    public AccessPolicyEntity createAccessPolicy(final Revision revision, final AccessPolicyDTO accessPolicyDTO) {
+        final Authorizable tenantAuthorizable = authorizableLookup.getTenant();
+        final String creator = NiFiUserUtils.getNiFiUserIdentity();
+
+        final AccessPolicy newAccessPolicy = accessPolicyDAO.createAccessPolicy(accessPolicyDTO);
+        final ComponentReferenceEntity componentReference = createComponentReferenceEntity(newAccessPolicy.getResource());
+        final AccessPolicyDTO newAccessPolicyDto = dtoFactory.createAccessPolicyDto(newAccessPolicy,
+                newAccessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()),
+                newAccessPolicy.getUsers().stream().map(userId -> {
+                    final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userId));
+                    return entityFactory.createTenantEntity(dtoFactory.createTenantDTO(userDAO.getUser(userId)), userRevision,
+                            dtoFactory.createPermissionsDto(tenantAuthorizable));
+                }).collect(Collectors.toSet()), componentReference);
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(accessPolicyDTO.getId()));
+        return entityFactory.createAccessPolicyEntity(newAccessPolicyDto, dtoFactory.createRevisionDTO(new FlowModification(revision, creator)), permissions);
+    }
+
+    @Override
+    public UserEntity createUser(final Revision revision, final UserDTO userDTO) {
+        final String creator = NiFiUserUtils.getNiFiUserIdentity();
+        final User newUser = userDAO.createUser(userDTO);
+        final Set<TenantEntity> tenantEntities = userGroupDAO.getUserGroupsForUser(newUser.getIdentifier()).stream()
+                .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet());
+        final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUser(newUser.getIdentifier()).stream()
+                .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+        final UserDTO newUserDto = dtoFactory.createUserDto(newUser, tenantEntities, policyEntities);
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant());
+        return entityFactory.createUserEntity(newUserDto, dtoFactory.createRevisionDTO(new FlowModification(revision, creator)), permissions);
+    }
+
+    private ComponentReferenceEntity createComponentReferenceEntity(final String resource) {
+        ComponentReferenceEntity componentReferenceEntity = null;
+        try {
+            // get the component authorizable
+            Authorizable componentAuthorizable = authorizableLookup.getAuthorizableFromResource(resource);
+
+            // if this represents an authorizable whose policy permissions are enforced through the base resource,
+            // get the underlying base authorizable for the component reference
+            if (componentAuthorizable instanceof EnforcePolicyPermissionsThroughBaseResource) {
+                componentAuthorizable = ((EnforcePolicyPermissionsThroughBaseResource) componentAuthorizable).getBaseAuthorizable();
+            }
+
+            final ComponentReferenceDTO componentReference = dtoFactory.createComponentReferenceDto(componentAuthorizable);
+            if (componentReference != null) {
+                final PermissionsDTO componentReferencePermissions = dtoFactory.createPermissionsDto(componentAuthorizable);
+                final RevisionDTO componentReferenceRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(componentReference.getId()));
+                componentReferenceEntity = entityFactory.createComponentReferenceEntity(componentReference, componentReferenceRevision, componentReferencePermissions);
+            }
+        } catch (final ResourceNotFoundException e) {
+            // component not found for the specified resource
+        }
+
+        return componentReferenceEntity;
+    }
+
+    private AccessPolicySummaryEntity createAccessPolicySummaryEntity(final AccessPolicy ap) {
+        final ComponentReferenceEntity componentReference = createComponentReferenceEntity(ap.getResource());
+        final AccessPolicySummaryDTO apSummary = dtoFactory.createAccessPolicySummaryDto(ap, componentReference);
+        final PermissionsDTO apPermissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(ap.getIdentifier()));
+        final RevisionDTO apRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(ap.getIdentifier()));
+        return entityFactory.createAccessPolicySummaryEntity(apSummary, apRevision, apPermissions);
+    }
+
+    @Override
+    public UserGroupEntity createUserGroup(final Revision revision, final UserGroupDTO userGroupDTO) {
+        final String creator = NiFiUserUtils.getNiFiUserIdentity();
+        final Group newUserGroup = userGroupDAO.createUserGroup(userGroupDTO);
+        final Set<TenantEntity> tenantEntities = newUserGroup.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet());
+        final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUserGroup(newUserGroup.getIdentifier()).stream()
+                .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+        final UserGroupDTO newUserGroupDto = dtoFactory.createUserGroupDto(newUserGroup, tenantEntities, policyEntities);
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant());
+        return entityFactory.createUserGroupEntity(newUserGroupDto, dtoFactory.createRevisionDTO(new FlowModification(revision, creator)), permissions);
+    }
+
+    private void validateSnippetContents(final FlowSnippetDTO flow) {
+        // validate any processors
+        if (flow.getProcessors() != null) {
+            for (final ProcessorDTO processorDTO : flow.getProcessors()) {
+                final ProcessorNode processorNode = processorDAO.getProcessor(processorDTO.getId());
+                processorDTO.setValidationStatus(processorNode.getValidationStatus().name());
+
+                final Collection<ValidationResult> validationErrors = processorNode.getValidationErrors();
+                if (validationErrors != null && !validationErrors.isEmpty()) {
+                    final List<String> errors = new ArrayList<>();
+                    for (final ValidationResult validationResult : validationErrors) {
+                        errors.add(validationResult.toString());
+                    }
+                    processorDTO.setValidationErrors(errors);
+                }
+            }
+        }
+
+        if (flow.getInputPorts() != null) {
+            for (final PortDTO portDTO : flow.getInputPorts()) {
+                final Port port = inputPortDAO.getPort(portDTO.getId());
+                final Collection<ValidationResult> validationErrors = port.getValidationErrors();
+                if (validationErrors != null && !validationErrors.isEmpty()) {
+                    final List<String> errors = new ArrayList<>();
+                    for (final ValidationResult validationResult : validationErrors) {
+                        errors.add(validationResult.toString());
+                    }
+                    portDTO.setValidationErrors(errors);
+                }
+            }
+        }
+
+        if (flow.getOutputPorts() != null) {
+            for (final PortDTO portDTO : flow.getOutputPorts()) {
+                final Port port = outputPortDAO.getPort(portDTO.getId());
+                final Collection<ValidationResult> validationErrors = port.getValidationErrors();
+                if (validationErrors != null && !validationErrors.isEmpty()) {
+                    final List<String> errors = new ArrayList<>();
+                    for (final ValidationResult validationResult : validationErrors) {
+                        errors.add(validationResult.toString());
+                    }
+                    portDTO.setValidationErrors(errors);
+                }
+            }
+        }
+
+        // get any remote process group issues
+        if (flow.getRemoteProcessGroups() != null) {
+            for (final RemoteProcessGroupDTO remoteProcessGroupDTO : flow.getRemoteProcessGroups()) {
+                final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupDTO.getId());
+
+                if (remoteProcessGroup.getAuthorizationIssue() != null) {
+                    remoteProcessGroupDTO.setAuthorizationIssues(Arrays.asList(remoteProcessGroup.getAuthorizationIssue()));
+                }
+            }
+        }
+    }
+
+    @Override
+    public FlowEntity copySnippet(final String groupId, final String snippetId, final Double originX, final Double originY, final String idGenerationSeed) {
+        // create the new snippet
+        final FlowSnippetDTO snippet = snippetDAO.copySnippet(groupId, snippetId, originX, originY, idGenerationSeed);
+
+        // save the flow
+        controllerFacade.save();
+
+        // drop the snippet
+        snippetDAO.dropSnippet(snippetId);
+
+        // post process new flow snippet
+        final FlowDTO flowDto = postProcessNewFlowSnippet(groupId, snippet);
+
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setFlow(flowDto);
+        return flowEntity;
+    }
+
+    @Override
+    public SnippetEntity createSnippet(final SnippetDTO snippetDTO) {
+        // add the component
+        final Snippet snippet = snippetDAO.createSnippet(snippetDTO);
+
+        // save the flow
+        controllerFacade.save();
+
+        final SnippetDTO dto = dtoFactory.createSnippetDto(snippet);
+        final RevisionUpdate<SnippetDTO> snapshot = new StandardRevisionUpdate<>(dto, null);
+
+        return entityFactory.createSnippetEntity(snapshot.getComponent());
+    }
+
+    @Override
+    public PortEntity createInputPort(final Revision revision, final String groupId, final PortDTO inputPortDTO) {
+        final RevisionUpdate<PortDTO> snapshot = createComponent(
+                revision,
+                inputPortDTO,
+                () -> inputPortDAO.createPort(groupId, inputPortDTO),
+                port -> dtoFactory.createPortDto(port));
+
+        final Port port = inputPortDAO.getPort(inputPortDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port));
+        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());
+        return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public PortEntity createOutputPort(final Revision revision, final String groupId, final PortDTO outputPortDTO) {
+        final RevisionUpdate<PortDTO> snapshot = createComponent(
+                revision,
+                outputPortDTO,
+                () -> outputPortDAO.createPort(groupId, outputPortDTO),
+                port -> dtoFactory.createPortDto(port));
+
+        final Port port = outputPortDAO.getPort(outputPortDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port));
+        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());
+        return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public ProcessGroupEntity createProcessGroup(final Revision revision, final String parentGroupId, final ProcessGroupDTO processGroupDTO) {
+        final RevisionUpdate<ProcessGroupDTO> snapshot = createComponent(
+                revision,
+                processGroupDTO,
+                () -> processGroupDAO.createProcessGroup(parentGroupId, processGroupDTO),
+                processGroup -> dtoFactory.createProcessGroupDto(processGroup));
+
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+        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(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, status, bulletinEntities);
+    }
+
+    @Override
+    public RemoteProcessGroupEntity createRemoteProcessGroup(final Revision revision, final String groupId, final RemoteProcessGroupDTO remoteProcessGroupDTO) {
+        final RevisionUpdate<RemoteProcessGroupDTO> snapshot = createComponent(
+                revision,
+                remoteProcessGroupDTO,
+                () -> remoteProcessGroupDAO.createRemoteProcessGroup(groupId, remoteProcessGroupDTO),
+                remoteProcessGroup -> dtoFactory.createRemoteProcessGroupDto(remoteProcessGroup));
+
+        final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroup));
+        final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(remoteProcessGroup, controllerFacade.getRemoteProcessGroupStatus(remoteProcessGroup.getIdentifier()));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroup.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createRemoteProcessGroupEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()),
+                permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public boolean isRemoteGroupPortConnected(final String remoteProcessGroupId, final String remotePortId) {
+        final RemoteProcessGroup rpg = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId);
+        RemoteGroupPort port = rpg.getInputPort(remotePortId);
+        if (port != null) {
+            return port.hasIncomingConnection();
+        }
+
+        port = rpg.getOutputPort(remotePortId);
+        if (port != null) {
+            return !port.getConnections().isEmpty();
+        }
+
+        throw new ResourceNotFoundException("Could not find Port with ID " + remotePortId + " as a child of RemoteProcessGroup with ID " + remoteProcessGroupId);
+    }
+
+    @Override
+    public void verifyCanAddTemplate(String groupId, String name) {
+        templateDAO.verifyCanAddTemplate(name, groupId);
+    }
+
+    @Override
+    public void verifyComponentTypes(FlowSnippetDTO snippet) {
+        templateDAO.verifyComponentTypes(snippet);
+    }
+
+    @Override
+    public void verifyComponentTypes(final VersionedProcessGroup versionedGroup) {
+        controllerFacade.verifyComponentTypes(versionedGroup);
+    }
+
+    @Override
+    public void verifyImportProcessGroup(final VersionControlInformationDTO versionControlInfo, final VersionedProcessGroup contents, final String groupId) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        verifyImportProcessGroup(versionControlInfo, contents, 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) {
+            // 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. "
+                    + "If Process Group A contains Process Group B, then Process Group B is not allowed to contain the flow identified by Process Group A.");
+            }
+        }
+
+        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
+    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);
+
+        // create the template
+        final TemplateDTO templateDTO = new TemplateDTO();
+        templateDTO.setName(name);
+        templateDTO.setDescription(description);
+        templateDTO.setTimestamp(new Date());
+        templateDTO.setSnippet(snippetUtils.populateFlowSnippet(snippet, true, true, true));
+        templateDTO.setEncodingVersion(TemplateDTO.MAX_ENCODING_VERSION);
+
+        // set the id based on the specified seed
+        final String uuid = idGenerationSeed.isPresent() ? (UUID.nameUUIDFromBytes(idGenerationSeed.get().getBytes(StandardCharsets.UTF_8))).toString() : UUID.randomUUID().toString();
+        templateDTO.setId(uuid);
+
+        // create the template
+        final Template template = templateDAO.createTemplate(templateDTO, groupId);
+
+        // drop the snippet
+        snippetDAO.dropSnippet(snippetId);
+
+        // save the flow
+        controllerFacade.save();
+
+        return dtoFactory.createTemplateDTO(template);
+    }
+
+    /**
+     * Ensures default values are populated for all components in this snippet. This is necessary to handle old templates without default values
+     * and when existing properties have default values introduced.
+     *
+     * @param snippet snippet
+     */
+    private void ensureDefaultPropertyValuesArePopulated(final FlowSnippetDTO snippet) {
+        if (snippet != null) {
+            if (snippet.getControllerServices() != null) {
+                snippet.getControllerServices().forEach(dto -> {
+                    if (dto.getProperties() == null) {
+                        dto.setProperties(new LinkedHashMap<>());
+                    }
+
+                    try {
+                        final ConfigurableComponent configurableComponent = controllerFacade.getTemporaryComponent(dto.getType(), dto.getBundle());
+                        configurableComponent.getPropertyDescriptors().forEach(descriptor -> {
+                            if (dto.getProperties().get(descriptor.getName()) == null) {
+                                dto.getProperties().put(descriptor.getName(), descriptor.getDefaultValue());
+                            }
+                        });
+                    } catch (final Exception e) {
+                        logger.warn(String.format("Unable to create ControllerService of type %s to populate default values.", dto.getType()));
+                    }
+                });
+            }
+
+            if (snippet.getProcessors() != null) {
+                snippet.getProcessors().forEach(dto -> {
+                    if (dto.getConfig() == null) {
+                        dto.setConfig(new ProcessorConfigDTO());
+                    }
+
+                    final ProcessorConfigDTO config = dto.getConfig();
+                    if (config.getProperties() == null) {
+                        config.setProperties(new LinkedHashMap<>());
+                    }
+
+                    try {
+                        final ConfigurableComponent configurableComponent = controllerFacade.getTemporaryComponent(dto.getType(), dto.getBundle());
+                        configurableComponent.getPropertyDescriptors().forEach(descriptor -> {
+                            if (config.getProperties().get(descriptor.getName()) == null) {
+                                config.getProperties().put(descriptor.getName(), descriptor.getDefaultValue());
+                            }
+                        });
+                    } catch (final Exception e) {
+                        logger.warn(String.format("Unable to create Processor of type %s to populate default values.", dto.getType()));
+                    }
+                });
+            }
+
+            if (snippet.getProcessGroups() != null) {
+                snippet.getProcessGroups().forEach(processGroup -> {
+                    ensureDefaultPropertyValuesArePopulated(processGroup.getContents());
+                });
+            }
+        }
+    }
+
+    @Override
+    public TemplateDTO importTemplate(final TemplateDTO templateDTO, final String groupId, final Optional<String> idGenerationSeed) {
+        // ensure id is set
+        final String uuid = idGenerationSeed.isPresent() ? (UUID.nameUUIDFromBytes(idGenerationSeed.get().getBytes(StandardCharsets.UTF_8))).toString() : UUID.randomUUID().toString();
+        templateDTO.setId(uuid);
+
+        // mark the timestamp
+        templateDTO.setTimestamp(new Date());
+
+        // ensure default values are populated
+        ensureDefaultPropertyValuesArePopulated(templateDTO.getSnippet());
+
+        // import the template
+        final Template template = templateDAO.importTemplate(templateDTO, groupId);
+
+        // save the flow
+        controllerFacade.save();
+
+        // return the template dto
+        return dtoFactory.createTemplateDTO(template);
+    }
+
+    /**
+     * Post processes a new flow snippet including validation, removing the snippet, and DTO conversion.
+     *
+     * @param groupId group id
+     * @param snippet snippet
+     * @return flow dto
+     */
+    private FlowDTO postProcessNewFlowSnippet(final String groupId, final FlowSnippetDTO snippet) {
+        // validate the new snippet
+        validateSnippetContents(snippet);
+
+        // identify all components added
+        final Set<String> identifiers = new HashSet<>();
+        snippet.getProcessors().stream()
+                .map(proc -> proc.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getConnections().stream()
+                .map(conn -> conn.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getInputPorts().stream()
+                .map(port -> port.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getOutputPorts().stream()
+                .map(port -> port.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getProcessGroups().stream()
+                .map(group -> group.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getRemoteProcessGroups().stream()
+                .map(remoteGroup -> remoteGroup.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getRemoteProcessGroups().stream()
+                .filter(remoteGroup -> remoteGroup.getContents() != null && remoteGroup.getContents().getInputPorts() != null)
+                .flatMap(remoteGroup -> remoteGroup.getContents().getInputPorts().stream())
+                .map(remoteInputPort -> remoteInputPort.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getRemoteProcessGroups().stream()
+                .filter(remoteGroup -> remoteGroup.getContents() != null && remoteGroup.getContents().getOutputPorts() != null)
+                .flatMap(remoteGroup -> remoteGroup.getContents().getOutputPorts().stream())
+                .map(remoteOutputPort -> remoteOutputPort.getId())
+                .forEach(id -> identifiers.add(id));
+        snippet.getLabels().stream()
+                .map(label -> label.getId())
+                .forEach(id -> identifiers.add(id));
+
+        final ProcessGroup group = processGroupDAO.getProcessGroup(groupId);
+        final ProcessGroupStatus groupStatus = controllerFacade.getProcessGroupStatus(groupId);
+        return dtoFactory.createFlowDto(group, groupStatus, snippet, revisionManager, this::getProcessGroupBulletins);
+    }
+
+    @Override
+    public FlowEntity createTemplateInstance(final String groupId, final Double originX, final Double originY, final String templateEncodingVersion,
+                                             final FlowSnippetDTO requestSnippet, final String idGenerationSeed) {
+
+        // instantiate the template - there is no need to make another copy of the flow snippet since the actual template
+        // was copied and this dto is only used to instantiate it's components (which as already completed)
+        final FlowSnippetDTO snippet = templateDAO.instantiateTemplate(groupId, originX, originY, templateEncodingVersion, requestSnippet, idGenerationSeed);
+
+        // save the flow
+        controllerFacade.save();
+
+        // post process the new flow snippet
+        final FlowDTO flowDto = postProcessNewFlowSnippet(groupId, snippet);
+
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setFlow(flowDto);
+        return flowEntity;
+    }
+
+    @Override
+    public ControllerServiceEntity createControllerService(final Revision revision, final String groupId, final ControllerServiceDTO controllerServiceDTO) {
+        controllerServiceDTO.setParentGroupId(groupId);
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // request claim for component to be created... revision already verified (version == 0)
+        final RevisionClaim claim = new StandardRevisionClaim(revision);
+
+        final RevisionUpdate<ControllerServiceDTO> snapshot;
+        if (groupId == null) {
+            // update revision through revision manager
+            snapshot = revisionManager.updateRevision(claim, user, () -> {
+                // Unfortunately, we can not use the createComponent() method here because createComponent() wants to obtain the read lock
+                // on the group. The Controller Service may or may not have a Process Group (it won't if it's controller-scoped).
+                final ControllerServiceNode controllerService = controllerServiceDAO.createControllerService(controllerServiceDTO);
+                controllerFacade.save();
+
+                awaitValidationCompletion(controllerService);
+                final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(controllerService);
+
+                final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
+                return new StandardRevisionUpdate<>(dto, lastMod);
+            });
+        } else {
+            snapshot = revisionManager.updateRevision(claim, user, () -> {
+                final ControllerServiceNode controllerService = controllerServiceDAO.createControllerService(controllerServiceDTO);
+                controllerFacade.save();
+
+                awaitValidationCompletion(controllerService);
+                final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(controllerService);
+
+                final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
+                return new StandardRevisionUpdate<>(dto, lastMod);
+            });
+        }
+
+        final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createControllerServiceEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities);
+    }
+
+    @Override
+    public ControllerServiceEntity updateControllerService(final Revision revision, final ControllerServiceDTO controllerServiceDTO) {
+        // get the component, ensure we have access to it, and perform the update request
+        final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
+        final RevisionUpdate<ControllerServiceDTO> snapshot = updateComponent(revision,
+                controllerService,
+                () -> controllerServiceDAO.updateControllerService(controllerServiceDTO),
+                cs -> {
+                    awaitValidationCompletion(cs);
+                    final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(cs);
+                    final ControllerServiceReference ref = controllerService.getReferences();
+                    final ControllerServiceReferencingComponentsEntity referencingComponentsEntity =
+                            createControllerServiceReferencingComponentsEntity(ref, Sets.newHashSet(controllerService.getIdentifier()));
+                    dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+                    return dto;
+                });
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createControllerServiceEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities);
+    }
+
+
+    @Override
+    public ControllerServiceReferencingComponentsEntity updateControllerServiceReferencingComponents(
+            final Map<String, Revision> referenceRevisions, final String controllerServiceId, final ScheduledState scheduledState, final ControllerServiceState controllerServiceState) {
+
+        final RevisionClaim claim = new StandardRevisionClaim(referenceRevisions.values());
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final RevisionUpdate<ControllerServiceReferencingComponentsEntity> update = revisionManager.updateRevision(claim, user,
+                new UpdateRevisionTask<ControllerServiceReferencingComponentsEntity>() {
+                    @Override
+                    public RevisionUpdate<ControllerServiceReferencingComponentsEntity> update() {
+                        final Set<ComponentNode> updated = controllerServiceDAO.updateControllerServiceReferencingComponents(controllerServiceId, scheduledState, controllerServiceState);
+                        final ControllerServiceReference updatedReference = controllerServiceDAO.getControllerService(controllerServiceId).getReferences();
+
+                        // get the revisions of the updated components
+                        final Map<String, Revision> updatedRevisions = new HashMap<>();
+                        for (final ComponentNode component : updated) {
+                            final Revision currentRevision = revisionManager.getRevision(component.getIdentifier());
+                            final Revision requestRevision = referenceRevisions.get(component.getIdentifier());
+                            updatedRevisions.put(component.getIdentifier(), currentRevision.incrementRevision(requestRevision.getClientId()));
+                        }
+
+                        // ensure the revision for all referencing components is included regardless of whether they were updated in this request
+                        for (final ComponentNode component : updatedReference.findRecursiveReferences(ComponentNode.class)) {
+                            updatedRevisions.putIfAbsent(component.getIdentifier(), revisionManager.getRevision(component.getIdentifier()));
+                        }
+
+                        final ControllerServiceReferencingComponentsEntity entity = createControllerServiceReferencingComponentsEntity(updatedReference, updatedRevisions);
+                        return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values()));
+                    }
+                });
+
+        return update.getComponent();
+    }
+
+    /**
+     * Finds the identifiers for all components referencing a ControllerService.
+     *
+     * @param reference      ControllerServiceReference
+     * @param visited        ControllerServices we've already visited
+     */
+    private void findControllerServiceReferencingComponentIdentifiers(final ControllerServiceReference reference, final Set<ControllerServiceNode> visited) {
+        for (final ComponentNode component : reference.getReferencingComponents()) {
+
+            // if this is a ControllerService consider it's referencing components
+            if (component instanceof ControllerServiceNode) {
+                final ControllerServiceNode node = (ControllerServiceNode) component;
+                if (!visited.contains(node)) {
+                    visited.add(node);
+                    findControllerServiceReferencingComponentIdentifiers(node.getReferences(), visited);
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates entities for components referencing a ControllerService using their current revision.
+     *
+     * @param reference ControllerServiceReference
+     * @return The entity
+     */
+    private ControllerServiceReferencingComponentsEntity createControllerServiceReferencingComponentsEntity(final ControllerServiceReference reference, final Set<String> lockedIds) {
+        final Set<ControllerServiceNode> visited = new HashSet<>();
+        visited.add(reference.getReferencedComponent());
+        findControllerServiceReferencingComponentIdentifiers(reference, visited);
+
+        final Map<String, Revision> referencingRevisions = new HashMap<>();
+        for (final ComponentNode component : reference.getReferencingComponents()) {
+            referencingRevisions.put(component.getIdentifier(), revisionManager.getRevision(component.getIdentifier()));
+        }
+
+        return createControllerServiceReferencingComponentsEntity(reference, referencingRevisions);
+    }
+
+    /**
+     * Creates entities for components referencing a ControllerService using the specified revisions.
+     *
+     * @param reference ControllerServiceReference
+     * @param revisions The revisions
+     * @return The entity
+     */
+    private ControllerServiceReferencingComponentsEntity createControllerServiceReferencingComponentsEntity(
+            final ControllerServiceReference reference, final Map<String, Revision> revisions) {
+        final Set<ControllerServiceNode> visited = new HashSet<>();
+        visited.add(reference.getReferencedComponent());
+        return createControllerServiceReferencingComponentsEntity(reference, revisions, visited);
+    }
+
+    /**
+     * Creates entities for components referencing a ControllerServcie using the specified revisions.
+     *
+     * @param reference ControllerServiceReference
+     * @param revisions The revisions
+     * @param visited   Which services we've already considered (in case of cycle)
+     * @return The entity
+     */
+    private ControllerServiceReferencingComponentsEntity createControllerServiceReferencingComponentsEntity(
+            final ControllerServiceReference reference, final Map<String, Revision> revisions, final Set<ControllerServiceNode> visited) {
+
+        final String modifier = NiFiUserUtils.getNiFiUserIdentity();
+        final Set<ComponentNode> referencingComponents = reference.getReferencingComponents();
+
+        final Set<ControllerServiceReferencingComponentEntity> componentEntities = new HashSet<>();
+        for (final ComponentNode refComponent : referencingComponents) {
+            final PermissionsDTO permissions = dtoFactory.createPermissionsDto(refComponent);
+            final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(refComponent));
+
+            final Revision revision = revisions.get(refComponent.getIdentifier());
+            final FlowModification flowMod = new FlowModification(revision, modifier);
+            final RevisionDTO revisionDto = dtoFactory.createRevisionDTO(flowMod);
+            final ControllerServiceReferencingComponentDTO dto = dtoFactory.createControllerServiceReferencingComponentDTO(refComponent);
+
+            if (refComponent instanceof ControllerServiceNode) {
+                final ControllerServiceNode node = (ControllerServiceNode) refComponent;
+
+                // indicate if we've hit a cycle
+                dto.setReferenceCycle(visited.contains(node));
+
+                // mark node as visited before building the reference cycle
+                visited.add(node);
+
+                // if we haven't encountered this service before include it's referencing components
+                if (!dto.getReferenceCycle()) {
+                    final ControllerServiceReference refReferences = node.getReferences();
+                    final Map<String, Revision> referencingRevisions = new HashMap<>(revisions);
+                    for (final ComponentNode component : refReferences.getReferencingComponents()) {
+                        referencingRevisions.putIfAbsent(component.getIdentifier(), revisionManager.getRevision(component.getIdentifier()));
+                    }
+                    final ControllerServiceReferencingComponentsEntity references = createControllerServiceReferencingComponentsEntity(refReferences, referencingRevisions, visited);
+                    dto.setReferencingComponents(references.getControllerServiceReferencingComponents());
+                }
+            }
+
+            componentEntities.add(entityFactory.createControllerServiceReferencingComponentEntity(refComponent.getIdentifier(), dto, revisionDto, permissions, operatePermissions));
+        }
+
+        final ControllerServiceReferencingComponentsEntity entity = new ControllerServiceReferencingComponentsEntity();
+        entity.setControllerServiceReferencingComponents(componentEntities);
+        return entity;
+    }
+
+    @Override
+    public ControllerServiceEntity deleteControllerService(final Revision revision, final String controllerServiceId) {
+        final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
+        final ControllerServiceDTO snapshot = deleteComponent(
+                revision,
+                controllerService.getResource(),
+                () -> controllerServiceDAO.deleteControllerService(controllerServiceId),
+                true,
+                dtoFactory.createControllerServiceDto(controllerService));
+
+        return entityFactory.createControllerServiceEntity(snapshot, null, permissions, operatePermissions, null);
+    }
+
+
+    @Override
+    public RegistryClientEntity createRegistryClient(Revision revision, RegistryDTO registryDTO) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // 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 createRegistryClientEntity(registry);
+    }
+
+    @Override
+    public RegistryClientEntity getRegistryClient(final String registryId) {
+        final FlowRegistry registry = registryDAO.getFlowRegistry(registryId);
+        return createRegistryClientEntity(registry);
+    }
+
+    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);
+
+        return entityFactory.createRegistryClientEntity(dto, revision, permissions);
+    }
+
+    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<RegistryClientEntity> getRegistryClients() {
+        return registryDAO.getFlowRegistries().stream()
+            .map(this::createRegistryClientEntity)
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<RegistryEntity> getRegistriesForUser(final NiFiUser user) {
+        return registryDAO.getFlowRegistriesForUser(user).stream()
+                .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(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 Permissions regPermissions = bucket.getPermissions();
+                    final PermissionsDTO permissions = new PermissionsDTO();
+                    permissions.setCanRead(regPermissions.getCanRead());
+                    permissions.setCanWrite(regPermissions.getCanWrite());
+
+                    return entityFactory.createBucketEntity(dto, permissions);
+                })
+                .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 RegistryClientEntity updateRegistryClient(Revision revision, RegistryDTO 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, () -> {
+            final boolean duplicateName = registryDAO.getFlowRegistries().stream()
+                    .anyMatch(reg -> reg.getName().equals(registryDTO.getName()) && !reg.getIdentifier().equals(registryDTO.getId()));
+
+            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());
+
+            controllerFacade.save();
+
+            final Revision updatedRevision = revisionManager.getRevision(revision.getComponentId()).incrementRevision(revision.getClientId());
+            final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity());
+
+            return new StandardRevisionUpdate<>(registry, lastModification);
+        });
+
+        final FlowRegistry updatedReg = revisionUpdate.getComponent();
+        return createRegistryClientEntity(updatedReg);
+    }
+
+    @Override
+    public void verifyDeleteRegistry(String registryId) {
+        processGroupDAO.verifyDeleteFlowRegistry(registryId);
+    }
+
+    @Override
+    public RegistryClientEntity deleteRegistryClient(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 createRegistryClientEntity(registry);
+    }
+
+    @Override
+    public ReportingTaskEntity createReportingTask(final Revision revision, final ReportingTaskDTO reportingTaskDTO) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // 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<ReportingTaskDTO> snapshot = revisionManager.updateRevision(claim, user, () -> {
+            // create the reporting task
+            final ReportingTaskNode reportingTask = reportingTaskDAO.createReportingTask(reportingTaskDTO);
+
+            // save the update
+            controllerFacade.save();
+            awaitValidationCompletion(reportingTask);
+
+            final ReportingTaskDTO dto = dtoFactory.createReportingTaskDto(reportingTask);
+            final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity());
+            return new StandardRevisionUpdate<>(dto, lastMod);
+        });
+
+        final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskDTO.getId());
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createReportingTaskEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities);
+    }
+
+    @Override
+    public ReportingTaskEntity updateReportingTask(final Revision revision, final ReportingTaskDTO reportingTaskDTO) {
+        // get the component, ensure we have access to it, and perform the update request
+        final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskDTO.getId());
+        final RevisionUpdate<ReportingTaskDTO> snapshot = updateComponent(revision,
+                reportingTask,
+                () -> reportingTaskDAO.updateReportingTask(reportingTaskDTO),
+                rt -> {
+                    awaitValidationCompletion(rt);
+                    return dtoFactory.createReportingTaskDto(rt);
+                });
+
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createReportingTaskEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities);
+    }
+
+    @Override
+    public ReportingTaskEntity deleteReportingTask(final Revision revision, final String reportingTaskId) {
+        final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask));
+        final ReportingTaskDTO snapshot = deleteComponent(
+                revision,
+                reportingTask.getResource(),
+                () -> reportingTaskDAO.deleteReportingTask(reportingTaskId),
+                true,
+                dtoFactory.createReportingTaskDto(reportingTask));
+
+        return entityFactory.createReportingTaskEntity(snapshot, null, permissions, operatePermissions, null);
+    }
+
+    @Override
+    public void deleteActions(final Date endDate) {
+        // get the user from the request
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new WebApplicationException(new Throwable("Unable to access details for current user."));
+        }
+
+        // create the purge details
+        final FlowChangePurgeDetails details = new FlowChangePurgeDetails();
+        details.setEndDate(endDate);
+
+        // create a purge action to record that records are being removed
+        final FlowChangeAction purgeAction = new FlowChangeAction();
+        purgeAction.setUserIdentity(user.getIdentity());
+        purgeAction.setOperation(Operation.Purge);
+        purgeAction.setTimestamp(new Date());
+        purgeAction.setSourceId("Flow Controller");
+        purgeAction.setSourceName("History");
+        purgeAction.setSourceType(Component.Controller);
+        purgeAction.setActionDetails(details);
+
+        // purge corresponding actions
+        auditService.purgeActions(endDate, purgeAction);
+    }
+
+    @Override
+    public ProvenanceDTO submitProvenance(final ProvenanceDTO query) {
+        return controllerFacade.submitProvenance(query);
+    }
+
+    @Override
+    public void deleteProvenance(final String queryId) {
+        controllerFacade.deleteProvenanceQuery(queryId);
+    }
+
+    @Override
+    public LineageDTO submitLineage(final LineageDTO lineage) {
+        return controllerFacade.submitLineage(lineage);
+    }
+
+    @Override
+    public void deleteLineage(final String lineageId) {
+        controllerFacade.deleteLineage(lineageId);
+    }
+
+    @Override
+    public ProvenanceEventDTO submitReplay(final Long eventId) {
+        return controllerFacade.submitReplay(eventId);
+    }
+
+    // -----------------------------------------
+    // Read Operations
+    // -----------------------------------------
+
+    @Override
+    public SearchResultsDTO searchController(final String query) {
+        return controllerFacade.search(query);
+    }
+
+    @Override
+    public DownloadableContent getContent(final String connectionId, final String flowFileUuid, final String uri) {
+        return connectionDAO.getContent(connectionId, flowFileUuid, uri);
+    }
+
+    @Override
+    public DownloadableContent getContent(final Long eventId, final String uri, final ContentDirection contentDirection) {
+        return controllerFacade.getContent(eventId, uri, contentDirection);
+    }
+
+    @Override
+    public ProvenanceDTO getProvenance(final String queryId, final Boolean summarize, final Boolean incrementalResults) {
+        return controllerFacade.getProvenanceQuery(queryId, summarize, incrementalResults);
+    }
+
+    @Override
+    public LineageDTO getLineage(final String lineageId) {
+        return controllerFacade.getLineage(lineageId);
+    }
+
+    @Override
+    public ProvenanceOptionsDTO getProvenanceSearchOptions() {
+        return controllerFacade.getProvenanceSearchOptions();
+    }
+
+    @Override
+    public ProvenanceEventDTO getProvenanceEvent(final Long id) {
+        return controllerFacade.getProvenanceEvent(id);
+    }
+
+    @Override
+    public ProcessGroupStatusEntity getProcessGroupStatus(final String groupId, final boolean recursive) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+        final ProcessGroupStatusDTO dto = dtoFactory.createProcessGroupStatusDto(processGroup, controllerFacade.getProcessGroupStatus(groupId));
+
+        // prune the response as necessary
+        if (!recursive) {
+            pruneChildGroups(dto.getAggregateSnapshot());
+            if (dto.getNodeSnapshots() != null) {
+                for (final NodeProcessGroupStatusSnapshotDTO nodeSnapshot : dto.getNodeSnapshots()) {
+                    pruneChildGroups(nodeSnapshot.getStatusSnapshot());
+                }
+            }
+        }
+
+        return entityFactory.createProcessGroupStatusEntity(dto, permissions);
+    }
+
+    private void pruneChildGroups(final ProcessGroupStatusSnapshotDTO snapshot) {
+        for (final ProcessGroupStatusSnapshotEntity childProcessGroupStatusEntity : snapshot.getProcessGroupStatusSnapshots()) {
+            final ProcessGroupStatusSnapshotDTO childProcessGroupStatus = childProcessGroupStatusEntity.getProcessGroupStatusSnapshot();
+            childProcessGroupStatus.setConnectionStatusSnapshots(null);
+            childProcessGroupStatus.setProcessGroupStatusSnapshots(null);
+            childProcessGroupStatus.setInputPortStatusSnapshots(null);
+            childProcessGroupStatus.setOutputPortStatusSnapshots(null);
+            childProcessGroupStatus.setProcessorStatusSnapshots(null);
+            childProcessGroupStatus.setRemoteProcessGroupStatusSnapshots(null);
+        }
+    }
+
+    @Override
+    public ControllerStatusDTO getControllerStatus() {
+        return controllerFacade.getControllerStatus();
+    }
+
+    @Override
+    public ComponentStateDTO getProcessorState(final String processorId) {
+        final StateMap clusterState = isClustered() ? processorDAO.getState(processorId, Scope.CLUSTER) : null;
+        final StateMap localState = processorDAO.getState(processorId, Scope.LOCAL);
+
+        // processor will be non null as it was already found when getting the state
+        final ProcessorNode processor = processorDAO.getProcessor(processorId);
+        return dtoFactory.createComponentStateDTO(processorId, processor.getProcessor().getClass(), localState, clusterState);
+    }
+
+    @Override
+    public ComponentStateDTO getControllerServiceState(final String controllerServiceId) {
+        final StateMap clusterState = isClustered() ? controllerServiceDAO.getState(controllerServiceId, Scope.CLUSTER) : null;
+        final StateMap localState = controllerServiceDAO.getState(controllerServiceId, Scope.LOCAL);
+
+        // controller service will be non null as it was already found when getting the state
+        final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId);
+        return dtoFactory.createComponentStateDTO(controllerServiceId, controllerService.getControllerServiceImplementation().getClass(), localState, clusterState);
+    }
+
+    @Override
+    public ComponentStateDTO getReportingTaskState(final String reportingTaskId) {
+        final StateMap clusterState = isClustered() ? reportingTaskDAO.getState(reportingTaskId, Scope.CLUSTER) : null;
+        final StateMap localState = reportingTaskDAO.getState(reportingTaskId, Scope.LOCAL);
+
+        // reporting task will be non null as it was already found when getting the state
+        final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskId);
+        return dtoFactory.createComponentStateDTO(reportingTaskId, reportingTask.getReportingTask().getClass(), localState, clusterState);
+    }
+
+    @Override
+    public CountersDTO getCounters() {
+        final List<Counter> counters = controllerFacade.getCounters();
+        final Set<CounterDTO> counterDTOs = new LinkedHashSet<>(counters.size());
+        for (final Counter counter : counters) {
+            counterDTOs.add(dtoFactory.createCounterDto(counter));
+        }
+
+        final CountersSnapshotDTO snapshotDto = dtoFactory.createCountersDto(counterDTOs);
+        final CountersDTO countersDto = new CountersDTO();
+        countersDto.setAggregateSnapshot(snapshotDto);
+
+        return countersDto;
+    }
+
+    private ConnectionEntity createConnectionEntity(final Connection connection) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(connection.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection);
+        final ConnectionStatusDTO status = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connection.getIdentifier()));
+        return entityFactory.createConnectionEntity(dtoFactory.createConnectionDto(connection), revision, permissions, status);
+    }
+
+    @Override
+    public Set<ConnectionEntity> getConnections(final String groupId) {
+        final Set<Connection> connections = connectionDAO.getConnections(groupId);
+        return connections.stream()
+            .map(connection -> createConnectionEntity(connection))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public ConnectionEntity getConnection(final String connectionId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        return createConnectionEntity(connection);
+    }
+
+    @Override
+    public DropRequestDTO getFlowFileDropRequest(final String connectionId, final String dropRequestId) {
+        return dtoFactory.createDropRequestDTO(connectionDAO.getFlowFileDropRequest(connectionId, dropRequestId));
+    }
+
+    @Override
+    public ListingRequestDTO getFlowFileListingRequest(final String connectionId, final String listingRequestId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        final ListingRequestDTO listRequest = dtoFactory.createListingRequestDTO(connectionDAO.getFlowFileListingRequest(connectionId, listingRequestId));
+
+        // include whether the source and destination are running
+        if (connection.getSource() != null) {
+            listRequest.setSourceRunning(connection.getSource().isRunning());
+        }
+        if (connection.getDestination() != null) {
+            listRequest.setDestinationRunning(connection.getDestination().isRunning());
+        }
+
+        return listRequest;
+    }
+
+    @Override
+    public FlowFileDTO getFlowFile(final String connectionId, final String flowFileUuid) {
+        return dtoFactory.createFlowFileDTO(connectionDAO.getFlowFile(connectionId, flowFileUuid));
+    }
+
+    @Override
+    public ConnectionStatusEntity getConnectionStatus(final String connectionId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection);
+        final ConnectionStatusDTO dto = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connectionId));
+        return entityFactory.createConnectionStatusEntity(dto, permissions);
+    }
+
+    @Override
+    public StatusHistoryEntity getConnectionStatusHistory(final String connectionId) {
+        final Connection connection = connectionDAO.getConnection(connectionId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection);
+        final StatusHistoryDTO dto = controllerFacade.getConnectionStatusHistory(connectionId);
+        return entityFactory.createStatusHistoryEntity(dto, permissions);
+    }
+
+    private ProcessorEntity createProcessorEntity(final ProcessorNode processor, final NiFiUser user) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processor.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor, user);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processor));
+        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());
+        return entityFactory.createProcessorEntity(dtoFactory.createProcessorDto(processor), revision, permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @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, user))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public TemplateDTO exportTemplate(final String id) {
+        final Template template = templateDAO.getTemplate(id);
+        final TemplateDTO templateDetails = template.getDetails();
+
+        final TemplateDTO templateDTO = dtoFactory.createTemplateDTO(template);
+        templateDTO.setSnippet(dtoFactory.copySnippetContents(templateDetails.getSnippet()));
+        return templateDTO;
+    }
+
+    @Override
+    public TemplateDTO getTemplate(final String id) {
+        return dtoFactory.createTemplateDTO(templateDAO.getTemplate(id));
+    }
+
+    @Override
+    public Set<TemplateEntity> getTemplates() {
+        return templateDAO.getTemplates().stream()
+                .map(template -> {
+                    final TemplateDTO dto = dtoFactory.createTemplateDTO(template);
+                    final PermissionsDTO permissions = dtoFactory.createPermissionsDto(template);
+
+                    final TemplateEntity entity = new TemplateEntity();
+                    entity.setId(dto.getId());
+                    entity.setPermissions(permissions);
+                    entity.setTemplate(dto);
+                    return entity;
+                }).collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<DocumentedTypeDTO> getWorkQueuePrioritizerTypes() {
+        return controllerFacade.getFlowFileComparatorTypes();
+    }
+
+    @Override
+    public Set<DocumentedTypeDTO> getProcessorTypes(final String bundleGroup, final String bundleArtifact, final String type) {
+        return controllerFacade.getFlowFileProcessorTypes(bundleGroup, bundleArtifact, type);
+    }
+
+    @Override
+    public Set<DocumentedTypeDTO> getControllerServiceTypes(final String serviceType, final String serviceBundleGroup, final String serviceBundleArtifact, final String serviceBundleVersion,
+                                                            final String bundleGroup, final String bundleArtifact, final String type) {
+        return controllerFacade.getControllerServiceTypes(serviceType, serviceBundleGroup, serviceBundleArtifact, serviceBundleVersion, bundleGroup, bundleArtifact, type);
+    }
+
+    @Override
+    public Set<DocumentedTypeDTO> getReportingTaskTypes(final String bundleGroup, final String bundleArtifact, final String type) {
+        return controllerFacade.getReportingTaskTypes(bundleGroup, bundleArtifact, type);
+    }
+
+    @Override
+    public ProcessorEntity getProcessor(final String id) {
+        final ProcessorNode processor = processorDAO.getProcessor(id);
+        return createProcessorEntity(processor, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public PropertyDescriptorDTO getProcessorPropertyDescriptor(final String id, final String property) {
+        final ProcessorNode processor = processorDAO.getProcessor(id);
+        PropertyDescriptor descriptor = processor.getPropertyDescriptor(property);
+
+        // return an invalid descriptor if the processor doesn't support this property
+        if (descriptor == null) {
+            descriptor = new PropertyDescriptor.Builder().name(property).addValidator(Validator.INVALID).dynamic(true).build();
+        }
+
+        return dtoFactory.createPropertyDescriptorDto(descriptor, processor.getProcessGroup().getIdentifier());
+    }
+
+    @Override
+    public ProcessorStatusEntity getProcessorStatus(final String id) {
+        final ProcessorNode processor = processorDAO.getProcessor(id);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor);
+        final ProcessorStatusDTO dto = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(id));
+        return entityFactory.createProcessorStatusEntity(dto, permissions);
+    }
+
+    @Override
+    public StatusHistoryEntity getProcessorStatusHistory(final String id) {
+        final ProcessorNode processor = processorDAO.getProcessor(id);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor);
+        final StatusHistoryDTO dto = controllerFacade.getProcessorStatusHistory(id);
+        return entityFactory.createStatusHistoryEntity(dto, permissions);
+    }
+
+    private boolean authorizeBulletin(final Bulletin bulletin) {
+        final String sourceId = bulletin.getSourceId();
+        final ComponentType type = bulletin.getSourceType();
+
+        final Authorizable authorizable;
+        try {
+            switch (type) {
+                case PROCESSOR:
+                    authorizable = authorizableLookup.getProcessor(sourceId).getAuthorizable();
+                    break;
+                case REPORTING_TASK:
+                    authorizable = authorizableLookup.getReportingTask(sourceId).getAuthorizable();
+                    break;
+                case CONTROLLER_SERVICE:
+                    authorizable = authorizableLookup.getControllerService(sourceId).getAuthorizable();
+                    break;
+                case FLOW_CONTROLLER:
+                    authorizable = controllerFacade;
+                    break;
+                case INPUT_PORT:
+                    authorizable = authorizableLookup.getInputPort(sourceId);
+                    break;
+                case OUTPUT_PORT:
+                    authorizable = authorizableLookup.getOutputPort(sourceId);
+                    break;
+                case REMOTE_PROCESS_GROUP:
+                    authorizable = authorizableLookup.getRemoteProcessGroup(sourceId);
+                    break;
+                default:
+                    throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this bulletin.").build());
+            }
+        } catch (final ResourceNotFoundException e) {
+            // if the underlying component is gone, disallow
+            return false;
+        }
+
+        // perform the authorization
+        final AuthorizationResult result = authorizable.checkAuthorization(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+        return Result.Approved.equals(result.getResult());
+    }
+
+    @Override
+    public BulletinBoardDTO getBulletinBoard(final BulletinQueryDTO query) {
+        // build the query
+        final BulletinQuery.Builder queryBuilder = new BulletinQuery.Builder()
+                .groupIdMatches(query.getGroupId())
+                .sourceIdMatches(query.getSourceId())
+                .nameMatches(query.getName())
+                .messageMatches(query.getMessage())
+                .after(query.getAfter())
+                .limit(query.getLimit());
+
+        // perform the query
+        final List<Bulletin> results = bulletinRepository.findBulletins(queryBuilder.build());
+
+        // perform the query and generate the results - iterating in reverse order since we are
+        // getting the most recent results by ordering by timestamp desc above. this gets the
+        // exact results we want but in reverse order
+        final List<BulletinEntity> bulletinEntities = new ArrayList<>();
+        for (final ListIterator<Bulletin> bulletinIter = results.listIterator(results.size()); bulletinIter.hasPrevious(); ) {
+            final Bulletin bulletin = bulletinIter.previous();
+            bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), authorizeBulletin(bulletin)));
+        }
+
+        // create the bulletin board
+        final BulletinBoardDTO bulletinBoard = new BulletinBoardDTO();
+        bulletinBoard.setBulletins(bulletinEntities);
+        bulletinBoard.setGenerated(new Date());
+        return bulletinBoard;
+    }
+
+    @Override
+    public SystemDiagnosticsDTO getSystemDiagnostics() {
+        final SystemDiagnostics sysDiagnostics = controllerFacade.getSystemDiagnostics();
+        return dtoFactory.createSystemDiagnosticsDto(sysDiagnostics);
+    }
+
+    @Override
+    public List<ResourceDTO> getResources() {
+        final List<Resource> resources = controllerFacade.getResources();
+        final List<ResourceDTO> resourceDtos = new ArrayList<>(resources.size());
+        for (final Resource resource : resources) {
+            resourceDtos.add(dtoFactory.createResourceDto(resource));
+        }
+        return resourceDtos;
+    }
+
+    @Override
+    public void discoverCompatibleBundles(VersionedProcessGroup versionedGroup) {
+        BundleUtils.discoverCompatibleBundles(controllerFacade.getExtensionManager(), versionedGroup);
+    }
+
+    @Override
+    public BundleCoordinate getCompatibleBundle(String type, BundleDTO bundleDTO) {
+        return BundleUtils.getCompatibleBundle(controllerFacade.getExtensionManager(), type, bundleDTO);
+    }
+
+    @Override
+    public ConfigurableComponent getTempComponent(String classType, BundleCoordinate bundleCoordinate) {
+        return controllerFacade.getExtensionManager().getTempComponent(classType, bundleCoordinate);
+    }
+
+    /**
+     * Ensures the specified user has permission to access the specified port. This method does
+     * not utilize the DataTransferAuthorizable as that will enforce the entire chain is
+     * authorized for the transfer. This method is only invoked when obtaining the site to site
+     * details so the entire chain isn't necessary.
+     */
+    private boolean isUserAuthorized(final NiFiUser user, final RootGroupPort port) {
+        final boolean isSiteToSiteSecure = Boolean.TRUE.equals(properties.isSiteToSiteSecure());
+
+        // if site to site is not secure, allow all users
+        if (!isSiteToSiteSecure) {
+            return true;
+        }
+
+        final Map<String, String> userContext;
+        if (user.getClientAddress() != null && !user.getClientAddress().trim().isEmpty()) {
+            userContext = new HashMap<>();
+            userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress());
+        } else {
+            userContext = null;
+        }
+
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .resource(ResourceFactory.getDataTransferResource(port.getResource()))
+                .identity(user.getIdentity())
+                .groups(user.getGroups())
+                .anonymous(user.isAnonymous())
+                .accessAttempt(false)
+                .action(RequestAction.WRITE)
+                .userContext(userContext)
+                .explanationSupplier(() -> "Unable to retrieve port details.")
+                .build();
+
+        final AuthorizationResult result = authorizer.authorize(request);
+        return Result.Approved.equals(result.getResult());
+    }
+
+    @Override
+    public ControllerDTO getSiteToSiteDetails() {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new WebApplicationException(new Throwable("Unable to access details for current user."));
+        }
+
+        // serialize the input ports this NiFi has access to
+        final Set<PortDTO> inputPortDtos = new LinkedHashSet<>();
+        final Set<RootGroupPort> inputPorts = controllerFacade.getInputPorts();
+        for (final RootGroupPort inputPort : inputPorts) {
+            if (isUserAuthorized(user, inputPort)) {
+                final PortDTO dto = new PortDTO();
+                dto.setId(inputPort.getIdentifier());
+                dto.setName(inputPort.getName());
+                dto.setComments(inputPort.getComments());
+                dto.setState(inputPort.getScheduledState().toString());
+                inputPortDtos.add(dto);
+            }
+        }
+
+        // serialize the output ports this NiFi has access to
+        final Set<PortDTO> outputPortDtos = new LinkedHashSet<>();
+        for (final RootGroupPort outputPort : controllerFacade.getOutputPorts()) {
+            if (isUserAuthorized(user, outputPort)) {
+                final PortDTO dto = new PortDTO();
+                dto.setId(outputPort.getIdentifier());
+                dto.setName(outputPort.getName());
+                dto.setComments(outputPort.getComments());
+                dto.setState(outputPort.getScheduledState().toString());
+                outputPortDtos.add(dto);
+            }
+        }
+
+        // get the root group
+        final ProcessGroup rootGroup = processGroupDAO.getProcessGroup(controllerFacade.getRootGroupId());
+        final ProcessGroupCounts counts = rootGroup.getCounts();
+
+        // create the controller dto
+        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());
+        controllerDTO.setOutputPortCount(outputPortDtos.size());
+        controllerDTO.setRunningCount(counts.getRunningCount());
+        controllerDTO.setStoppedCount(counts.getStoppedCount());
+        controllerDTO.setInvalidCount(counts.getInvalidCount());
+        controllerDTO.setDisabledCount(counts.getDisabledCount());
+
+        // determine the site to site configuration
+        controllerDTO.setRemoteSiteListeningPort(controllerFacade.getRemoteSiteListeningPort());
+        controllerDTO.setRemoteSiteHttpListeningPort(controllerFacade.getRemoteSiteListeningHttpPort());
+        controllerDTO.setSiteToSiteSecure(controllerFacade.isRemoteSiteCommsSecure());
+
+        return controllerDTO;
+    }
+
+    @Override
+    public ControllerConfigurationEntity getControllerConfiguration() {
+        final Revision rev = revisionManager.getRevision(FlowController.class.getSimpleName());
+        final ControllerConfigurationDTO dto = dtoFactory.createControllerConfigurationDto(controllerFacade);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerFacade);
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(rev);
+        return entityFactory.createControllerConfigurationEntity(dto, revision, permissions);
+    }
+
+    @Override
+    public ControllerBulletinsEntity getControllerBulletins() {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final ControllerBulletinsEntity controllerBulletinsEntity = new ControllerBulletinsEntity();
+
+        final List<BulletinEntity> controllerBulletinEntities = new ArrayList<>();
+
+        final Authorizable controllerAuthorizable = authorizableLookup.getController();
+        final boolean authorized = controllerAuthorizable.isAuthorized(authorizer, RequestAction.READ, user);
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForController());
+        controllerBulletinEntities.addAll(bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, authorized)).collect(Collectors.toList()));
+
+        // get the controller service bulletins
+        final BulletinQuery controllerServiceQuery = new BulletinQuery.Builder().sourceType(ComponentType.CONTROLLER_SERVICE).build();
+        final List<Bulletin> allControllerServiceBulletins = bulletinRepository.findBulletins(controllerServiceQuery);
+        final List<BulletinEntity> controllerServiceBulletinEntities = new ArrayList<>();
+        for (final Bulletin bulletin : allControllerServiceBulletins) {
+            try {
+                final Authorizable controllerServiceAuthorizable = authorizableLookup.getControllerService(bulletin.getSourceId()).getAuthorizable();
+                final boolean controllerServiceAuthorized = controllerServiceAuthorizable.isAuthorized(authorizer, RequestAction.READ, user);
+
+                final BulletinEntity controllerServiceBulletin = entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), controllerServiceAuthorized);
+                controllerServiceBulletinEntities.add(controllerServiceBulletin);
+                controllerBulletinEntities.add(controllerServiceBulletin);
+            } catch (final ResourceNotFoundException e) {
+                // controller service missing.. skip
+            }
+        }
+        controllerBulletinsEntity.setControllerServiceBulletins(controllerServiceBulletinEntities);
+
+        // get the reporting task bulletins
+        final BulletinQuery reportingTaskQuery = new BulletinQuery.Builder().sourceType(ComponentType.REPORTING_TASK).build();
+        final List<Bulletin> allReportingTaskBulletins = bulletinRepository.findBulletins(reportingTaskQuery);
+        final List<BulletinEntity> reportingTaskBulletinEntities = new ArrayList<>();
+        for (final Bulletin bulletin : allReportingTaskBulletins) {
+            try {
+                final Authorizable reportingTaskAuthorizable = authorizableLookup.getReportingTask(bulletin.getSourceId()).getAuthorizable();
+                final boolean reportingTaskAuthorizableAuthorized = reportingTaskAuthorizable.isAuthorized(authorizer, RequestAction.READ, user);
+
+                final BulletinEntity reportingTaskBulletin = entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), reportingTaskAuthorizableAuthorized);
+                reportingTaskBulletinEntities.add(reportingTaskBulletin);
+                controllerBulletinEntities.add(reportingTaskBulletin);
+            } catch (final ResourceNotFoundException e) {
+                // reporting task missing.. skip
+            }
+        }
+        controllerBulletinsEntity.setReportingTaskBulletins(reportingTaskBulletinEntities);
+
+        controllerBulletinsEntity.setBulletins(pruneAndSortBulletins(controllerBulletinEntities, BulletinRepository.MAX_BULLETINS_FOR_CONTROLLER));
+        return controllerBulletinsEntity;
+    }
+
+    @Override
+    public FlowConfigurationEntity getFlowConfiguration() {
+        final FlowConfigurationDTO dto = dtoFactory.createFlowConfigurationDto(properties.getAutoRefreshInterval(),
+                properties.getDefaultBackPressureObjectThreshold(), properties.getDefaultBackPressureDataSizeThreshold(),properties.getDcaeDistributorApiHostname());
+        final FlowConfigurationEntity entity = new FlowConfigurationEntity();
+        entity.setFlowConfiguration(dto);
+        return entity;
+    }
+
+    @Override
+    public AccessPolicyEntity getAccessPolicy(final String accessPolicyId) {
+        final AccessPolicy accessPolicy = accessPolicyDAO.getAccessPolicy(accessPolicyId);
+        return createAccessPolicyEntity(accessPolicy);
+    }
+
+    @Override
+    public AccessPolicyEntity getAccessPolicy(final RequestAction requestAction, final String resource) {
+        Authorizable authorizable;
+        try {
+            authorizable = authorizableLookup.getAuthorizableFromResource(resource);
+        } catch (final ResourceNotFoundException e) {
+            // unable to find the underlying authorizable... user authorized based on top level /policies... create
+            // an anonymous authorizable to attempt to locate an existing policy for this resource
+            authorizable = new Authorizable() {
+                @Override
+                public Authorizable getParentAuthorizable() {
+                    return null;
+                }
+
+                @Override
+                public Resource getResource() {
+                    return new Resource() {
+                        @Override
+                        public String getIdentifier() {
+                            return resource;
+                        }
+
+                        @Override
+                        public String getName() {
+                            return resource;
+                        }
+
+                        @Override
+                        public String getSafeDescription() {
+                            return "Policy " + resource;
+                        }
+                    };
+                }
+            };
+        }
+
+        final AccessPolicy accessPolicy = accessPolicyDAO.getAccessPolicy(requestAction, authorizable);
+        return createAccessPolicyEntity(accessPolicy);
+    }
+
+    private AccessPolicyEntity createAccessPolicyEntity(final AccessPolicy accessPolicy) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(accessPolicy.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(accessPolicy.getIdentifier()));
+        final ComponentReferenceEntity componentReference = createComponentReferenceEntity(accessPolicy.getResource());
+        return entityFactory.createAccessPolicyEntity(
+                dtoFactory.createAccessPolicyDto(accessPolicy,
+                        accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()),
+                        accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()), componentReference),
+                revision, permissions);
+    }
+
+    @Override
+    public UserEntity getUser(final String userId) {
+        final User user = userDAO.getUser(userId);
+        return createUserEntity(user, true);
+    }
+
+    @Override
+    public Set<UserEntity> getUsers() {
+        final Set<User> users = userDAO.getUsers();
+        return users.stream()
+            .map(user -> createUserEntity(user, false))
+            .collect(Collectors.toSet());
+    }
+
+    private UserEntity createUserEntity(final User user, final boolean enforceUserExistence) {
+        final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(user.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant());
+        final Set<TenantEntity> userGroups = userGroupDAO.getUserGroupsForUser(user.getIdentifier()).stream()
+                .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(enforceUserExistence)).collect(Collectors.toSet());
+        final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUser(user.getIdentifier()).stream()
+                .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+        return entityFactory.createUserEntity(dtoFactory.createUserDto(user, userGroups, policyEntities), userRevision, permissions);
+    }
+
+    private UserGroupEntity createUserGroupEntity(final Group userGroup, final boolean enforceGroupExistence) {
+        final RevisionDTO userGroupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userGroup.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant());
+        final Set<TenantEntity> users = userGroup.getUsers().stream().map(mapUserIdToTenantEntity(enforceGroupExistence)).collect(Collectors.toSet());
+        final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUserGroup(userGroup.getIdentifier()).stream()
+                .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet());
+        return entityFactory.createUserGroupEntity(dtoFactory.createUserGroupDto(userGroup, users, policyEntities), userGroupRevision, permissions);
+    }
+
+    @Override
+    public UserGroupEntity getUserGroup(final String userGroupId) {
+        final Group userGroup = userGroupDAO.getUserGroup(userGroupId);
+        return createUserGroupEntity(userGroup, true);
+    }
+
+    @Override
+    public Set<UserGroupEntity> getUserGroups() {
+        final Set<Group> userGroups = userGroupDAO.getUserGroups();
+        return userGroups.stream()
+            .map(userGroup -> createUserGroupEntity(userGroup, false))
+            .collect(Collectors.toSet());
+    }
+
+    private LabelEntity createLabelEntity(final Label label) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(label.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(label);
+        return entityFactory.createLabelEntity(dtoFactory.createLabelDto(label), revision, permissions);
+    }
+
+    @Override
+    public Set<LabelEntity> getLabels(final String groupId) {
+        final Set<Label> labels = labelDAO.getLabels(groupId);
+        return labels.stream()
+            .map(label -> createLabelEntity(label))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public LabelEntity getLabel(final String labelId) {
+        final Label label = labelDAO.getLabel(labelId);
+        return createLabelEntity(label);
+    }
+
+    private FunnelEntity createFunnelEntity(final Funnel funnel) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(funnel.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnel);
+        return entityFactory.createFunnelEntity(dtoFactory.createFunnelDto(funnel), revision, permissions);
+    }
+
+    @Override
+    public Set<FunnelEntity> getFunnels(final String groupId) {
+        final Set<Funnel> funnels = funnelDAO.getFunnels(groupId);
+        return funnels.stream()
+            .map(funnel -> createFunnelEntity(funnel))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public FunnelEntity getFunnel(final String funnelId) {
+        final Funnel funnel = funnelDAO.getFunnel(funnelId);
+        return createFunnelEntity(funnel);
+    }
+
+    private PortEntity createInputPortEntity(final Port port) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(port.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port, NiFiUserUtils.getNiFiUser());
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port), NiFiUserUtils.getNiFiUser());
+        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());
+        return entityFactory.createPortEntity(dtoFactory.createPortDto(port), revision, permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    private PortEntity createOutputPortEntity(final Port port) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(port.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port, NiFiUserUtils.getNiFiUser());
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port), NiFiUserUtils.getNiFiUser());
+        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());
+        return entityFactory.createPortEntity(dtoFactory.createPortDto(port), revision, permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public Set<PortEntity> getInputPorts(final String groupId) {
+        final Set<Port> inputPorts = inputPortDAO.getPorts(groupId);
+        return inputPorts.stream()
+            .map(port -> createInputPortEntity(port))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<PortEntity> getOutputPorts(final String groupId) {
+        final Set<Port> ports = outputPortDAO.getPorts(groupId);
+        return ports.stream()
+            .map(port -> createOutputPortEntity(port))
+            .collect(Collectors.toSet());
+    }
+
+    private ProcessGroupEntity createProcessGroupEntity(final ProcessGroup group) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(group.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(group);
+        final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(group.getIdentifier()));
+        final List<BulletinEntity> bulletins = getProcessGroupBulletins(group);
+        return entityFactory.createProcessGroupEntity(dtoFactory.createProcessGroupDto(group), revision, permissions, status, bulletins);
+    }
+
+    private List<BulletinEntity> getProcessGroupBulletins(final ProcessGroup group) {
+        final List<Bulletin> bulletins = new ArrayList<>(bulletinRepository.findBulletinsForGroupBySource(group.getIdentifier()));
+
+        for (final ProcessGroup descendantGroup : group.findAllProcessGroups()) {
+            bulletins.addAll(bulletinRepository.findBulletinsForGroupBySource(descendantGroup.getIdentifier()));
+        }
+
+        List<BulletinEntity> bulletinEntities = new ArrayList<>();
+        for (final Bulletin bulletin : bulletins) {
+            bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), authorizeBulletin(bulletin)));
+        }
+
+        return pruneAndSortBulletins(bulletinEntities, BulletinRepository.MAX_BULLETINS_PER_COMPONENT);
+    }
+
+    private List<BulletinEntity> pruneAndSortBulletins(final List<BulletinEntity> bulletinEntities, final int maxBulletins) {
+        // sort the bulletins
+        Collections.sort(bulletinEntities, new Comparator<BulletinEntity>() {
+            @Override
+            public int compare(BulletinEntity o1, BulletinEntity o2) {
+                if (o1 == null && o2 == null) {
+                    return 0;
+                }
+                if (o1 == null) {
+                    return 1;
+                }
+                if (o2 == null) {
+                    return -1;
+                }
+
+                return -Long.compare(o1.getId(), o2.getId());
+            }
+        });
+
+        // prune the response to only include the max number of bulletins
+        if (bulletinEntities.size() > maxBulletins) {
+            return bulletinEntities.subList(0, maxBulletins);
+        } else {
+            return bulletinEntities;
+        }
+    }
+
+    @Override
+    public Set<ProcessGroupEntity> getProcessGroups(final String parentGroupId) {
+        final Set<ProcessGroup> groups = processGroupDAO.getProcessGroups(parentGroupId);
+        return groups.stream()
+            .map(group -> createProcessGroupEntity(group))
+            .collect(Collectors.toSet());
+    }
+
+    private RemoteProcessGroupEntity createRemoteGroupEntity(final RemoteProcessGroup rpg, final NiFiUser user) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(rpg.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(rpg, user);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(rpg), user);
+        final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(rpg, 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());
+        return entityFactory.createRemoteProcessGroupEntity(dtoFactory.createRemoteProcessGroupDto(rpg), revision, permissions, operatePermissions, status, bulletinEntities);
+    }
+
+    @Override
+    public Set<RemoteProcessGroupEntity> getRemoteProcessGroups(final String groupId) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final Set<RemoteProcessGroup> rpgs = remoteProcessGroupDAO.getRemoteProcessGroups(groupId);
+        return rpgs.stream()
+            .map(rpg -> createRemoteGroupEntity(rpg, user))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public PortEntity getInputPort(final String inputPortId) {
+        final Port port = inputPortDAO.getPort(inputPortId);
+        return createInputPortEntity(port);
+    }
+
+    @Override
+    public PortStatusEntity getInputPortStatus(final String inputPortId) {
+        final Port inputPort = inputPortDAO.getPort(inputPortId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(inputPort);
+        final PortStatusDTO dto = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(inputPortId));
+        return entityFactory.createPortStatusEntity(dto, permissions);
+    }
+
+    @Override
+    public PortEntity getOutputPort(final String outputPortId) {
+        final Port port = outputPortDAO.getPort(outputPortId);
+        return createOutputPortEntity(port);
+    }
+
+    @Override
+    public PortStatusEntity getOutputPortStatus(final String outputPortId) {
+        final Port outputPort = outputPortDAO.getPort(outputPortId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(outputPort);
+        final PortStatusDTO dto = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(outputPortId));
+        return entityFactory.createPortStatusEntity(dto, permissions);
+    }
+
+    @Override
+    public RemoteProcessGroupEntity getRemoteProcessGroup(final String remoteProcessGroupId) {
+        final RemoteProcessGroup rpg = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId);
+        return createRemoteGroupEntity(rpg, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public RemoteProcessGroupStatusEntity getRemoteProcessGroupStatus(final String id) {
+        final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(id);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup);
+        final RemoteProcessGroupStatusDTO dto = dtoFactory.createRemoteProcessGroupStatusDto(remoteProcessGroup, controllerFacade.getRemoteProcessGroupStatus(id));
+        return entityFactory.createRemoteProcessGroupStatusEntity(dto, permissions);
+    }
+
+    @Override
+    public StatusHistoryEntity getRemoteProcessGroupStatusHistory(final String id) {
+        final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(id);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup);
+        final StatusHistoryDTO dto = controllerFacade.getRemoteProcessGroupStatusHistory(id);
+        return entityFactory.createStatusHistoryEntity(dto, permissions);
+    }
+
+    @Override
+    public CurrentUserEntity getCurrentUser() {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final CurrentUserEntity entity = new CurrentUserEntity();
+        entity.setIdentity(user.getIdentity());
+        entity.setAnonymous(user.isAnonymous());
+        entity.setProvenancePermissions(dtoFactory.createPermissionsDto(authorizableLookup.getProvenance()));
+        entity.setCountersPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getCounters()));
+        entity.setTenantsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getTenant()));
+        entity.setControllerPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getController()));
+        entity.setPoliciesPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getPolicies()));
+        entity.setSystemPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getSystem()));
+        entity.setCanVersionFlows(CollectionUtils.isNotEmpty(flowRegistryClient.getRegistryIdentifiers()));
+
+        entity.setRestrictedComponentsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getRestrictedComponents()));
+
+        final Set<ComponentRestrictionPermissionDTO> componentRestrictionPermissions = new HashSet<>();
+        Arrays.stream(RequiredPermission.values()).forEach(requiredPermission -> {
+            final PermissionsDTO restrictionPermissions = dtoFactory.createPermissionsDto(authorizableLookup.getRestrictedComponents(requiredPermission));
+
+            final RequiredPermissionDTO requiredPermissionDto = new RequiredPermissionDTO();
+            requiredPermissionDto.setId(requiredPermission.getPermissionIdentifier());
+            requiredPermissionDto.setLabel(requiredPermission.getPermissionLabel());
+
+            final ComponentRestrictionPermissionDTO componentRestrictionPermissionDto = new ComponentRestrictionPermissionDTO();
+            componentRestrictionPermissionDto.setRequiredPermission(requiredPermissionDto);
+            componentRestrictionPermissionDto.setPermissions(restrictionPermissions);
+
+            componentRestrictionPermissions.add(componentRestrictionPermissionDto);
+        });
+        entity.setComponentRestrictionPermissions(componentRestrictionPermissions);
+
+        return entity;
+    }
+
+    @Override
+    public ProcessGroupFlowEntity getProcessGroupFlow(final String groupId) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+
+        // Get the Process Group Status but we only need a status depth of one because for any child process group,
+        // we ignore the status of each individual components. I.e., if Process Group A has child Group B, and child Group B
+        // has a Processor, we don't care about the individual stats of that Processor because the ProcessGroupFlowEntity
+        // doesn't include that anyway. So we can avoid including the information in the status that is returned.
+        final ProcessGroupStatus groupStatus = controllerFacade.getProcessGroupStatus(groupId, 1);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+        return entityFactory.createProcessGroupFlowEntity(dtoFactory.createProcessGroupFlowDto(processGroup, groupStatus, revisionManager, this::getProcessGroupBulletins), permissions);
+    }
+
+    @Override
+    public ProcessGroupEntity getProcessGroup(final String groupId) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        return createProcessGroupEntity(processGroup);
+    }
+
+    private ControllerServiceEntity createControllerServiceEntity(final ControllerServiceNode serviceNode, final Set<String> serviceIds) {
+        final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(serviceNode);
+
+        final ControllerServiceReference ref = serviceNode.getReferences();
+        final ControllerServiceReferencingComponentsEntity referencingComponentsEntity = createControllerServiceReferencingComponentsEntity(ref, serviceIds);
+        dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
+
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(serviceNode.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(serviceNode, NiFiUserUtils.getNiFiUser());
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(serviceNode), NiFiUserUtils.getNiFiUser());
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(serviceNode.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createControllerServiceEntity(dto, revision, permissions, operatePermissions, bulletinEntities);
+    }
+
+    @Override
+    public VariableRegistryEntity getVariableRegistry(final String groupId, final boolean includeAncestorGroups) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        if (processGroup == null) {
+            throw new ResourceNotFoundException("Could not find group with ID " + groupId);
+        }
+
+        return createVariableRegistryEntity(processGroup, includeAncestorGroups);
+    }
+
+    private VariableRegistryEntity createVariableRegistryEntity(final ProcessGroup processGroup, final boolean includeAncestorGroups) {
+        final VariableRegistryDTO registryDto = dtoFactory.createVariableRegistryDto(processGroup, revisionManager);
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+
+        if (includeAncestorGroups) {
+            ProcessGroup parent = processGroup.getParent();
+            while (parent != null) {
+                final PermissionsDTO parentPerms = dtoFactory.createPermissionsDto(parent);
+                if (Boolean.TRUE.equals(parentPerms.getCanRead())) {
+                    final VariableRegistryDTO parentRegistryDto = dtoFactory.createVariableRegistryDto(parent, revisionManager);
+                    final Set<VariableEntity> parentVariables = parentRegistryDto.getVariables();
+                    registryDto.getVariables().addAll(parentVariables);
+                }
+
+                parent = parent.getParent();
+            }
+        }
+
+        return entityFactory.createVariableRegistryEntity(registryDto, revision, permissions);
+    }
+
+    @Override
+    public VariableRegistryEntity populateAffectedComponents(final VariableRegistryDTO variableRegistryDto) {
+        final String groupId = variableRegistryDto.getProcessGroupId();
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        if (processGroup == null) {
+            throw new ResourceNotFoundException("Could not find group with ID " + groupId);
+        }
+
+        final VariableRegistryDTO registryDto = dtoFactory.populateAffectedComponents(variableRegistryDto, processGroup, revisionManager);
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+        return entityFactory.createVariableRegistryEntity(registryDto, revision, permissions);
+    }
+
+    @Override
+    public Set<ControllerServiceEntity> getControllerServices(final String groupId, final boolean includeAncestorGroups, final boolean includeDescendantGroups) {
+        final Set<ControllerServiceNode> serviceNodes = controllerServiceDAO.getControllerServices(groupId, includeAncestorGroups, includeDescendantGroups);
+        final Set<String> serviceIds = serviceNodes.stream().map(service -> service.getIdentifier()).collect(Collectors.toSet());
+
+        return serviceNodes.stream()
+            .map(serviceNode -> createControllerServiceEntity(serviceNode, serviceIds))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public ControllerServiceEntity getControllerService(final String controllerServiceId) {
+        final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId);
+        return createControllerServiceEntity(controllerService, Sets.newHashSet(controllerServiceId));
+    }
+
+    @Override
+    public PropertyDescriptorDTO getControllerServicePropertyDescriptor(final String id, final String property) {
+        final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(id);
+        PropertyDescriptor descriptor = controllerService.getControllerServiceImplementation().getPropertyDescriptor(property);
+
+        // return an invalid descriptor if the controller service doesn't support this property
+        if (descriptor == null) {
+            descriptor = new PropertyDescriptor.Builder().name(property).addValidator(Validator.INVALID).dynamic(true).build();
+        }
+
+        final String groupId = controllerService.getProcessGroup() == null ? null : controllerService.getProcessGroup().getIdentifier();
+        return dtoFactory.createPropertyDescriptorDto(descriptor, groupId);
+    }
+
+    @Override
+    public ControllerServiceReferencingComponentsEntity getControllerServiceReferencingComponents(final String controllerServiceId) {
+        final ControllerServiceNode service = controllerServiceDAO.getControllerService(controllerServiceId);
+        final ControllerServiceReference ref = service.getReferences();
+        return createControllerServiceReferencingComponentsEntity(ref, Sets.newHashSet(controllerServiceId));
+    }
+
+    private ReportingTaskEntity createReportingTaskEntity(final ReportingTaskNode reportingTask) {
+        final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(reportingTask.getIdentifier()));
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask);
+        final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask));
+        final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier()));
+        final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+        return entityFactory.createReportingTaskEntity(dtoFactory.createReportingTaskDto(reportingTask), revision, permissions, operatePermissions, bulletinEntities);
+    }
+
+    @Override
+    public Set<ReportingTaskEntity> getReportingTasks() {
+        final Set<ReportingTaskNode> reportingTasks = reportingTaskDAO.getReportingTasks();
+        return reportingTasks.stream()
+            .map(reportingTask -> createReportingTaskEntity(reportingTask))
+            .collect(Collectors.toSet());
+    }
+
+    @Override
+    public ReportingTaskEntity getReportingTask(final String reportingTaskId) {
+        final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskId);
+        return createReportingTaskEntity(reportingTask);
+    }
+
+    @Override
+    public PropertyDescriptorDTO getReportingTaskPropertyDescriptor(final String id, final String property) {
+        final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(id);
+        PropertyDescriptor descriptor = reportingTask.getReportingTask().getPropertyDescriptor(property);
+
+        // return an invalid descriptor if the reporting task doesn't support this property
+        if (descriptor == null) {
+            descriptor = new PropertyDescriptor.Builder().name(property).addValidator(Validator.INVALID).dynamic(true).build();
+        }
+
+        return dtoFactory.createPropertyDescriptorDto(descriptor, null);
+    }
+
+    @Override
+    public StatusHistoryEntity getProcessGroupStatusHistory(final String groupId) {
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
+        final StatusHistoryDTO dto = controllerFacade.getProcessGroupStatusHistory(groupId);
+        return entityFactory.createStatusHistoryEntity(dto, permissions);
+    }
+
+    @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);
+
+        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
+            registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, versionedFlowDto.getComments(), expectedVersion);
+        } catch (final NiFiRegistryException e) {
+            throw new IllegalArgumentException(e.getLocalizedMessage());
+        } catch (final IOException ioe) {
+            throw new IllegalStateException("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(bucket.getIdentifier());
+        vci.setBucketName(bucket.getName());
+        vci.setFlowId(flow.getIdentifier());
+        vci.setFlowName(flow.getName());
+        vci.setFlowDescription(flow.getDescription());
+        vci.setGroupId(groupId);
+        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);
+
+        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 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);
+        }
+
+        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
+    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(processGroup);
+        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(controllerFacade.getExtensionManager());
+        final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, false);
+        return versionedGroup;
+    }
+
+    @Override
+    public FlowComparisonEntity getLocalModifications(final String processGroupId) {
+        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;
+        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(controllerFacade.getExtensionManager());
+        final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, true);
+        final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents();
+
+        final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localGroup);
+        final ComparableDataFlow registryFlow = new StandardComparableDataFlow("Versioned Flow", registryGroup);
+
+        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 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(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());
+        }
+
+        return ancestorServiceIds;
+    }
+
+    @Override
+    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);
+        }
+
+        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 {
+        final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId);
+        if (registry == null) {
+            throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId);
+        }
+
+        return registry.getVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public VersionedFlowSnapshot registerVersionedFlowSnapshot(final String registryId, final VersionedFlow flow,
+        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);
+        }
+
+        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
+    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));
+
+        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()));
+    }
+
+    @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 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 ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId);
+
+        final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper(controllerFacade.getExtensionManager());
+        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 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()
+            .filter(difference -> difference.getDifferenceType() != DifferenceType.COMPONENT_ADDED) // components that are added are not components that will be affected in the local flow.
+            .filter(difference -> difference.getDifferenceType() != DifferenceType.BUNDLE_CHANGED)
+            .filter(FlowDifferenceFilters.FILTER_ADDED_REMOVED_REMOTE_PORTS)
+            .filter(FlowDifferenceFilters.FILTER_IGNORABLE_VERSIONED_FLOW_COORDINATE_CHANGES)
+            .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);
+            })
+            .collect(Collectors.toCollection(HashSet::new));
+
+        for (final FlowDifference difference : comparison.getDifferences()) {
+            // Ignore these as local differences for now because we can't do anything with it
+            if (difference.getDifferenceType() == DifferenceType.BUNDLE_CHANGED) {
+                continue;
+            }
+
+            // Ignore differences for adding remote ports
+            if (FlowDifferenceFilters.isAddedOrRemovedRemotePort(difference)) {
+                continue;
+            }
+
+            if (FlowDifferenceFilters.isIgnorableVersionedFlowCoordinateChange(difference)) {
+                continue;
+            }
+
+            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))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllFunnels().stream()
+                    .map(comp -> createAffectedComponentEntity(comp))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllInputPorts().stream()
+                    .map(comp -> createAffectedComponentEntity(comp))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllOutputPorts().stream()
+                    .map(comp -> createAffectedComponentEntity(comp))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllRemoteProcessGroups().stream()
+                    .flatMap(rpg -> Stream.concat(rpg.getInputPorts().stream(), rpg.getOutputPorts().stream()))
+                    .map(comp -> createAffectedComponentEntity(comp))
+                    .forEach(affectedComponents::add);
+                localGroup.findAllControllerServices().stream()
+                    .map(comp -> createAffectedComponentEntity(comp))
+                    .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));
+                }
+
+                final List<ProcessorNode> referencingProcessors = serviceNode.getReferences().findRecursiveReferences(ProcessorNode.class);
+                for (final ProcessorNode referencingProcessor : referencingProcessors) {
+                    affectedComponents.add(createAffectedComponentEntity(referencingProcessor));
+                }
+            }
+        }
+
+        // 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) {
+                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));
+                }
+            }
+
+            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));
+                }
+            }
+        }
+
+        return affectedComponents;
+    }
+
+    private void mapToConnectableId(final Collection<? extends Connectable> connectables, final Map<String, List<Connectable>> destination) {
+        for (final Connectable connectable : connectables) {
+            final Optional<String> versionedIdOption = connectable.getVersionedComponentId();
+
+            // Determine the Versioned ID by using the ID that is assigned, if one is. Otherwise,
+            // we will calculate the Versioned ID. This allows us to map connectables that currently are not under
+            // version control. We have to do this so that if we are changing flow versions and have a component that is running and it does not exist
+            // in the Versioned Flow, we still need to be able to create an AffectedComponentDTO for it.
+            final String versionedId;
+            if (versionedIdOption.isPresent()) {
+                versionedId = versionedIdOption.get();
+            } else {
+                versionedId = UUID.nameUUIDFromBytes(connectable.getIdentifier().getBytes(StandardCharsets.UTF_8)).toString();
+            }
+
+            final List<Connectable> byVersionedId = destination.computeIfAbsent(versionedId, key -> new ArrayList<>());
+            byVersionedId.add(connectable);
+        }
+    }
+
+
+    private AffectedComponentEntity createAffectedComponentEntity(final Connectable connectable) {
+        final AffectedComponentEntity entity = new AffectedComponentEntity();
+        entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(connectable.getIdentifier())));
+        entity.setId(connectable.getIdentifier());
+
+        final Authorizable authorizable = getAuthorizable(connectable);
+        final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable);
+        entity.setPermissions(permissionsDto);
+
+        final AffectedComponentDTO dto = new AffectedComponentDTO();
+        dto.setId(connectable.getIdentifier());
+        dto.setReferenceType(connectable.getConnectableType().name());
+        dto.setState(connectable.getScheduledState().name());
+
+        final String groupId = connectable instanceof RemoteGroupPort ? ((RemoteGroupPort) connectable).getRemoteProcessGroup().getIdentifier() : connectable.getProcessGroupIdentifier();
+        dto.setProcessGroupId(groupId);
+
+        entity.setComponent(dto);
+        return entity;
+    }
+
+    private AffectedComponentEntity createAffectedComponentEntity(final ControllerServiceNode serviceNode) {
+        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);
+        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 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);
+        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 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();
+
+        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(componentId);
+        }
+
+        return null;
+    }
+
+    @Override
+    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());
+        }
+
+        final VersionedFlowSnapshot snapshot;
+        try {
+            snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion(), fetchRemoteFlows, NiFiUserUtils.getNiFiUser());
+        } catch (final NiFiRegistryException | IOException e) {
+            logger.error(e.getMessage(), 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());
+        }
+
+        return snapshot;
+    }
+
+    @Override
+    public String getFlowRegistryName(final String flowRegistryId) {
+        final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(flowRegistryId);
+        return flowRegistry == null ? flowRegistryId : flowRegistry.getName();
+    }
+
+    private List<Revision> getComponentRevisions(final ProcessGroup processGroup, final boolean includeGroupRevision) {
+        final List<Revision> revisions = new ArrayList<>();
+        if (includeGroupRevision) {
+            revisions.add(revisionManager.getRevision(processGroup.getIdentifier()));
+        }
+
+        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 Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo,
+        final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) {
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
+        final List<Revision> revisions = getComponentRevisions(processGroup, false);
+        revisions.add(revision);
+
+        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, 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(revisionUpdate.getComponent(), updatedRevision, permissions, status, bulletinEntities);
+    }
+
+    private AuthorizationResult authorizeAction(final Action action) {
+        final String sourceId = action.getSourceId();
+        final Component type = action.getSourceType();
+
+        Authorizable authorizable;
+        try {
+            switch (type) {
+                case Processor:
+                    authorizable = authorizableLookup.getProcessor(sourceId).getAuthorizable();
+                    break;
+                case ReportingTask:
+                    authorizable = authorizableLookup.getReportingTask(sourceId).getAuthorizable();
+                    break;
+                case ControllerService:
+                    authorizable = authorizableLookup.getControllerService(sourceId).getAuthorizable();
+                    break;
+                case Controller:
+                    authorizable = controllerFacade;
+                    break;
+                case InputPort:
+                    authorizable = authorizableLookup.getInputPort(sourceId);
+                    break;
+                case OutputPort:
+                    authorizable = authorizableLookup.getOutputPort(sourceId);
+                    break;
+                case ProcessGroup:
+                    authorizable = authorizableLookup.getProcessGroup(sourceId).getAuthorizable();
+                    break;
+                case RemoteProcessGroup:
+                    authorizable = authorizableLookup.getRemoteProcessGroup(sourceId);
+                    break;
+                case Funnel:
+                    authorizable = authorizableLookup.getFunnel(sourceId);
+                    break;
+                case Connection:
+                    authorizable = authorizableLookup.getConnection(sourceId).getAuthorizable();
+                    break;
+                case AccessPolicy:
+                    authorizable = authorizableLookup.getAccessPolicyById(sourceId);
+                    break;
+                case User:
+                case UserGroup:
+                    authorizable = authorizableLookup.getTenant();
+                    break;
+                default:
+                    throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this action.").build());
+            }
+        } catch (final ResourceNotFoundException e) {
+            // if the underlying component is gone, use the controller to see if permissions should be allowed
+            authorizable = controllerFacade;
+        }
+
+        // perform the authorization
+        return authorizable.checkAuthorization(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+    }
+
+    @Override
+    public HistoryDTO getActions(final HistoryQueryDTO historyQueryDto) {
+        // extract the query criteria
+        final HistoryQuery historyQuery = new HistoryQuery();
+        historyQuery.setStartDate(historyQueryDto.getStartDate());
+        historyQuery.setEndDate(historyQueryDto.getEndDate());
+        historyQuery.setSourceId(historyQueryDto.getSourceId());
+        historyQuery.setUserIdentity(historyQueryDto.getUserIdentity());
+        historyQuery.setOffset(historyQueryDto.getOffset());
+        historyQuery.setCount(historyQueryDto.getCount());
+        historyQuery.setSortColumn(historyQueryDto.getSortColumn());
+        historyQuery.setSortOrder(historyQueryDto.getSortOrder());
+
+        // perform the query
+        final History history = auditService.getActions(historyQuery);
+
+        // only retain authorized actions
+        final HistoryDTO historyDto = dtoFactory.createHistoryDto(history);
+        if (history.getActions() != null) {
+            final List<ActionEntity> actionEntities = new ArrayList<>();
+            for (final Action action : history.getActions()) {
+                final AuthorizationResult result = authorizeAction(action);
+                actionEntities.add(entityFactory.createActionEntity(dtoFactory.createActionDto(action), Result.Approved.equals(result.getResult())));
+            }
+            historyDto.setActions(actionEntities);
+        }
+
+        // create the response
+        return historyDto;
+    }
+
+    @Override
+    public ActionEntity getAction(final Integer actionId) {
+        // get the action
+        final Action action = auditService.getAction(actionId);
+
+        // ensure the action was found
+        if (action == null) {
+            throw new ResourceNotFoundException(String.format("Unable to find action with id '%s'.", actionId));
+        }
+
+        final AuthorizationResult result = authorizeAction(action);
+        final boolean authorized = Result.Approved.equals(result.getResult());
+        if (!authorized) {
+            throw new AccessDeniedException(result.getExplanation());
+        }
+
+        // return the action
+        return entityFactory.createActionEntity(dtoFactory.createActionDto(action), authorized);
+    }
+
+    @Override
+    public ComponentHistoryDTO getComponentHistory(final String componentId) {
+        final Map<String, PropertyHistoryDTO> propertyHistoryDtos = new LinkedHashMap<>();
+        final Map<String, List<PreviousValue>> propertyHistory = auditService.getPreviousValues(componentId);
+
+        for (final Map.Entry<String, List<PreviousValue>> entry : propertyHistory.entrySet()) {
+            final List<PreviousValueDTO> previousValueDtos = new ArrayList<>();
+
+            for (final PreviousValue previousValue : entry.getValue()) {
+                final PreviousValueDTO dto = new PreviousValueDTO();
+                dto.setPreviousValue(previousValue.getPreviousValue());
+                dto.setTimestamp(previousValue.getTimestamp());
+                dto.setUserIdentity(previousValue.getUserIdentity());
+                previousValueDtos.add(dto);
+            }
+
+            if (!previousValueDtos.isEmpty()) {
+                final PropertyHistoryDTO propertyHistoryDto = new PropertyHistoryDTO();
+                propertyHistoryDto.setPreviousValues(previousValueDtos);
+                propertyHistoryDtos.put(entry.getKey(), propertyHistoryDto);
+            }
+        }
+
+        final ComponentHistoryDTO history = new ComponentHistoryDTO();
+        history.setComponentId(componentId);
+        history.setPropertyHistory(propertyHistoryDtos);
+
+        return history;
+    }
+
+    @Override
+    public ProcessorDiagnosticsEntity getProcessorDiagnostics(final String id) {
+        final ProcessorNode processor = processorDAO.getProcessor(id);
+        final ProcessorStatus processorStatus = controllerFacade.getProcessorStatus(id);
+
+        // Generate Processor Diagnostics
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final ProcessorDiagnosticsDTO dto = controllerFacade.getProcessorDiagnostics(processor, processorStatus, bulletinRepository, serviceId -> {
+            final ControllerServiceNode serviceNode = controllerServiceDAO.getControllerService(serviceId);
+            return createControllerServiceEntity(serviceNode, Collections.emptySet());
+        });
+
+        // Filter anything out of diagnostics that the user is not authorized to see.
+        final List<JVMDiagnosticsSnapshotDTO> jvmDiagnosticsSnaphots = new ArrayList<>();
+        final JVMDiagnosticsDTO jvmDiagnostics = dto.getJvmDiagnostics();
+        jvmDiagnosticsSnaphots.add(jvmDiagnostics.getAggregateSnapshot());
+
+        // filter controller-related information
+        final boolean canReadController = authorizableLookup.getController().isAuthorized(authorizer, RequestAction.READ, user);
+        if (!canReadController) {
+            for (final JVMDiagnosticsSnapshotDTO snapshot : jvmDiagnosticsSnaphots) {
+                snapshot.setControllerDiagnostics(null);
+            }
+        }
+
+        // filter system diagnostics information
+        final boolean canReadSystem = authorizableLookup.getSystem().isAuthorized(authorizer, RequestAction.READ, user);
+        if (!canReadSystem) {
+            for (final JVMDiagnosticsSnapshotDTO snapshot : jvmDiagnosticsSnaphots) {
+                snapshot.setSystemDiagnosticsDto(null);
+            }
+        }
+
+        final boolean canReadFlow = authorizableLookup.getFlow().isAuthorized(authorizer, RequestAction.READ, user);
+        if (!canReadFlow) {
+            for (final JVMDiagnosticsSnapshotDTO snapshot : jvmDiagnosticsSnaphots) {
+                snapshot.setFlowDiagnosticsDto(null);
+            }
+        }
+
+        // filter connections
+        final Predicate<ConnectionDiagnosticsDTO> connectionAuthorized = connectionDiagnostics -> {
+            final String connectionId = connectionDiagnostics.getConnection().getId();
+            return authorizableLookup.getConnection(connectionId).getAuthorizable().isAuthorized(authorizer, RequestAction.READ, user);
+        };
+
+        // Filter incoming connections by what user is authorized to READ
+        final Set<ConnectionDiagnosticsDTO> incoming = dto.getIncomingConnections();
+        final Set<ConnectionDiagnosticsDTO> filteredIncoming = incoming.stream()
+            .filter(connectionAuthorized)
+            .collect(Collectors.toSet());
+
+        dto.setIncomingConnections(filteredIncoming);
+
+        // Filter outgoing connections by what user is authorized to READ
+        final Set<ConnectionDiagnosticsDTO> outgoing = dto.getOutgoingConnections();
+        final Set<ConnectionDiagnosticsDTO> filteredOutgoing = outgoing.stream()
+            .filter(connectionAuthorized)
+            .collect(Collectors.toSet());
+        dto.setOutgoingConnections(filteredOutgoing);
+
+        // Filter out any controller services that are referenced by the Processor
+        final Set<ControllerServiceDiagnosticsDTO> referencedServices = dto.getReferencedControllerServices();
+        final Set<ControllerServiceDiagnosticsDTO> filteredReferencedServices = referencedServices.stream()
+            .filter(csDiagnostics -> {
+                final String csId = csDiagnostics.getControllerService().getId();
+                return authorizableLookup.getControllerService(csId).getAuthorizable().isAuthorized(authorizer, RequestAction.READ, user);
+            })
+            .map(csDiagnostics -> {
+                // Filter out any referencing components because those are generally not relevant from this context.
+                final ControllerServiceDTO serviceDto = csDiagnostics.getControllerService().getComponent();
+                if (serviceDto != null) {
+                    serviceDto.setReferencingComponents(null);
+                }
+                return csDiagnostics;
+            })
+            .collect(Collectors.toSet());
+        dto.setReferencedControllerServices(filteredReferencedServices);
+
+        final Revision revision = revisionManager.getRevision(id);
+        final RevisionDTO revisionDto = dtoFactory.createRevisionDTO(revision);
+        final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(processor);
+        final List<BulletinEntity> bulletins = bulletinRepository.findBulletinsForSource(id).stream()
+            .map(bulletin -> dtoFactory.createBulletinDto(bulletin))
+            .map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissionsDto.getCanRead()))
+            .collect(Collectors.toList());
+
+        final ProcessorStatusDTO processorStatusDto = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processor.getIdentifier()));
+        return entityFactory.createProcessorDiagnosticsEntity(dto, revisionDto, permissionsDto, processorStatusDto, bulletins);
+    }
+
+    @Override
+    public boolean isClustered() {
+        return controllerFacade.isClustered();
+    }
+
+    @Override
+    public String getNodeId() {
+        final NodeIdentifier nodeId = controllerFacade.getNodeId();
+        if (nodeId != null) {
+            return nodeId.getId();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public ClusterDTO getCluster() {
+        // create cluster summary dto
+        final ClusterDTO clusterDto = new ClusterDTO();
+
+        // set current time
+        clusterDto.setGenerated(new Date());
+
+        // create node dtos
+        final List<NodeDTO> nodeDtos = clusterCoordinator.getNodeIdentifiers().stream()
+            .map(nodeId -> getNode(nodeId))
+            .collect(Collectors.toList());
+        clusterDto.setNodes(nodeDtos);
+
+        return clusterDto;
+    }
+
+    @Override
+    public NodeDTO getNode(final String nodeId) {
+        final NodeIdentifier nodeIdentifier = clusterCoordinator.getNodeIdentifier(nodeId);
+        return getNode(nodeIdentifier);
+    }
+
+    private NodeDTO getNode(final NodeIdentifier nodeId) {
+        final NodeConnectionStatus nodeStatus = clusterCoordinator.getConnectionStatus(nodeId);
+        final List<NodeEvent> events = clusterCoordinator.getNodeEvents(nodeId);
+        final Set<String> roles = getRoles(nodeId);
+        final NodeHeartbeat heartbeat = heartbeatMonitor.getLatestHeartbeat(nodeId);
+        return dtoFactory.createNodeDTO(nodeId, nodeStatus, heartbeat, events, roles);
+    }
+
+    private Set<String> getRoles(final NodeIdentifier nodeId) {
+        final Set<String> roles = new HashSet<>();
+        final String nodeAddress = nodeId.getSocketAddress() + ":" + nodeId.getSocketPort();
+
+        for (final String roleName : ClusterRoles.getAllRoles()) {
+            final String leader = leaderElectionManager.getLeader(roleName);
+            if (leader == null) {
+                continue;
+            }
+
+            if (leader.equals(nodeAddress)) {
+                roles.add(roleName);
+            }
+        }
+
+        return roles;
+    }
+
+    @Override
+    public void deleteNode(final String nodeId) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new WebApplicationException(new Throwable("Unable to access details for current user."));
+        }
+
+        final String userDn = user.getIdentity();
+        final NodeIdentifier nodeIdentifier = clusterCoordinator.getNodeIdentifier(nodeId);
+        if (nodeIdentifier == null) {
+            throw new UnknownNodeException("Cannot remove Node with ID " + nodeId + " because it is not part of the cluster");
+        }
+
+        final NodeConnectionStatus nodeConnectionStatus = clusterCoordinator.getConnectionStatus(nodeIdentifier);
+        if (!nodeConnectionStatus.getState().equals(NodeConnectionState.OFFLOADED) && !nodeConnectionStatus.getState().equals(NodeConnectionState.DISCONNECTED)) {
+            throw new IllegalNodeDeletionException("Cannot remove Node with ID " + nodeId +
+                    " because it is not disconnected or offloaded, current state = " + nodeConnectionStatus.getState());
+        }
+
+        clusterCoordinator.removeNode(nodeIdentifier, userDn);
+        heartbeatMonitor.removeHeartbeat(nodeIdentifier);
+    }
+
+    /* reusable function declarations for converting ids to tenant entities */
+    private Function<String, TenantEntity> mapUserGroupIdToTenantEntity(final boolean enforceGroupExistence) {
+        return userGroupId -> {
+            final RevisionDTO userGroupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userGroupId));
+
+            final Group group;
+            if (enforceGroupExistence || userGroupDAO.hasUserGroup(userGroupId)) {
+                group = userGroupDAO.getUserGroup(userGroupId);
+            } else {
+                group = new Group.Builder().identifier(userGroupId).name("Group ID - " + userGroupId + " (removed externally)").build();
+            }
+
+            return entityFactory.createTenantEntity(dtoFactory.createTenantDTO(group), userGroupRevision,
+                    dtoFactory.createPermissionsDto(authorizableLookup.getTenant()));
+        };
+    }
+
+    private Function<String, TenantEntity> mapUserIdToTenantEntity(final boolean enforceUserExistence) {
+        return userId -> {
+            final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userId));
+
+            final User user;
+            if (enforceUserExistence || userDAO.hasUser(userId)) {
+                user = userDAO.getUser(userId);
+            } else {
+                user = new User.Builder().identifier(userId).identity("User ID - " + userId + " (removed externally)").build();
+            }
+
+            return entityFactory.createTenantEntity(dtoFactory.createTenantDTO(user), userRevision,
+                    dtoFactory.createPermissionsDto(authorizableLookup.getTenant()));
+        };
+    }
+
+
+    /* setters */
+    public void setProperties(final NiFiProperties properties) {
+        this.properties = properties;
+    }
+
+    public void setControllerFacade(final ControllerFacade controllerFacade) {
+        this.controllerFacade = controllerFacade;
+    }
+
+    public void setRemoteProcessGroupDAO(final RemoteProcessGroupDAO remoteProcessGroupDAO) {
+        this.remoteProcessGroupDAO = remoteProcessGroupDAO;
+    }
+
+    public void setLabelDAO(final LabelDAO labelDAO) {
+        this.labelDAO = labelDAO;
+    }
+
+    public void setFunnelDAO(final FunnelDAO funnelDAO) {
+        this.funnelDAO = funnelDAO;
+    }
+
+    public void setSnippetDAO(final SnippetDAO snippetDAO) {
+        this.snippetDAO = snippetDAO;
+    }
+
+    public void setProcessorDAO(final ProcessorDAO processorDAO) {
+        this.processorDAO = processorDAO;
+    }
+
+    public void setConnectionDAO(final ConnectionDAO connectionDAO) {
+        this.connectionDAO = connectionDAO;
+    }
+
+    public void setAuditService(final AuditService auditService) {
+        this.auditService = auditService;
+    }
+
+    public void setRevisionManager(final RevisionManager revisionManager) {
+        this.revisionManager = revisionManager;
+    }
+
+    public void setDtoFactory(final DtoFactory dtoFactory) {
+        this.dtoFactory = dtoFactory;
+    }
+
+    public void setEntityFactory(final EntityFactory entityFactory) {
+        this.entityFactory = entityFactory;
+    }
+
+    public void setInputPortDAO(final PortDAO inputPortDAO) {
+        this.inputPortDAO = inputPortDAO;
+    }
+
+    public void setOutputPortDAO(final PortDAO outputPortDAO) {
+        this.outputPortDAO = outputPortDAO;
+    }
+
+    public void setProcessGroupDAO(final ProcessGroupDAO processGroupDAO) {
+        this.processGroupDAO = processGroupDAO;
+    }
+
+    public void setControllerServiceDAO(final ControllerServiceDAO controllerServiceDAO) {
+        this.controllerServiceDAO = controllerServiceDAO;
+    }
+
+    public void setReportingTaskDAO(final ReportingTaskDAO reportingTaskDAO) {
+        this.reportingTaskDAO = reportingTaskDAO;
+    }
+
+    public void setTemplateDAO(final TemplateDAO templateDAO) {
+        this.templateDAO = templateDAO;
+    }
+
+    public void setSnippetUtils(final SnippetUtils snippetUtils) {
+        this.snippetUtils = snippetUtils;
+    }
+
+    public void setAuthorizableLookup(final AuthorizableLookup authorizableLookup) {
+        this.authorizableLookup = authorizableLookup;
+    }
+
+    public void setAuthorizer(final Authorizer authorizer) {
+        this.authorizer = authorizer;
+    }
+
+    public void setUserDAO(final UserDAO userDAO) {
+        this.userDAO = userDAO;
+    }
+
+    public void setUserGroupDAO(final UserGroupDAO userGroupDAO) {
+        this.userGroupDAO = userGroupDAO;
+    }
+
+    public void setAccessPolicyDAO(final AccessPolicyDAO accessPolicyDAO) {
+        this.accessPolicyDAO = accessPolicyDAO;
+    }
+
+    public void setClusterCoordinator(final ClusterCoordinator coordinator) {
+        this.clusterCoordinator = coordinator;
+    }
+
+    public void setHeartbeatMonitor(final HeartbeatMonitor heartbeatMonitor) {
+        this.heartbeatMonitor = heartbeatMonitor;
+    }
+
+    public void setBulletinRepository(final BulletinRepository bulletinRepository) {
+        this.bulletinRepository = bulletinRepository;
+    }
+
+    public void setLeaderElectionManager(final LeaderElectionManager leaderElectionManager) {
+        this.leaderElectionManager = leaderElectionManager;
+    }
+
+    public void setRegistryDAO(RegistryDAO registryDao) {
+        this.registryDAO = registryDao;
+    }
+
+    public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) {
+        this.flowRegistryClient = flowRegistryClient;
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
new file mode 100644 (file)
index 0000000..2943e10
--- /dev/null
@@ -0,0 +1,4354 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.web.api.dto;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.action.Action;
+import org.apache.nifi.action.component.details.ComponentDetails;
+import org.apache.nifi.action.component.details.ExtensionDetails;
+import org.apache.nifi.action.component.details.FlowChangeExtensionDetails;
+import org.apache.nifi.action.component.details.FlowChangeRemoteProcessGroupDetails;
+import org.apache.nifi.action.component.details.RemoteProcessGroupDetails;
+import org.apache.nifi.action.details.ActionDetails;
+import org.apache.nifi.action.details.ConfigureDetails;
+import org.apache.nifi.action.details.ConnectDetails;
+import org.apache.nifi.action.details.FlowChangeConfigureDetails;
+import org.apache.nifi.action.details.FlowChangeConnectDetails;
+import org.apache.nifi.action.details.FlowChangeMoveDetails;
+import org.apache.nifi.action.details.FlowChangePurgeDetails;
+import org.apache.nifi.action.details.MoveDetails;
+import org.apache.nifi.action.details.PurgeDetails;
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.annotation.behavior.Restriction;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.authorization.AccessPolicy;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.AuthorizerCapabilityDetection;
+import org.apache.nifi.authorization.Group;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.Resource;
+import org.apache.nifi.authorization.User;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.ComponentAuthorizable;
+import org.apache.nifi.authorization.resource.OperationAuthorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.bundle.BundleDetails;
+import org.apache.nifi.cluster.coordination.heartbeat.NodeHeartbeat;
+import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus;
+import org.apache.nifi.cluster.event.NodeEvent;
+import org.apache.nifi.cluster.manager.StatusMerger;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.ConnectableType;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Funnel;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.connectable.Position;
+import org.apache.nifi.controller.ActiveThreadInfo;
+import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.Counter;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ReportingTaskNode;
+import org.apache.nifi.controller.Snippet;
+import org.apache.nifi.controller.Template;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.queue.DropFlowFileState;
+import org.apache.nifi.controller.queue.DropFlowFileStatus;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.queue.FlowFileSummary;
+import org.apache.nifi.controller.queue.ListFlowFileState;
+import org.apache.nifi.controller.queue.ListFlowFileStatus;
+import org.apache.nifi.controller.queue.LoadBalanceStrategy;
+import org.apache.nifi.controller.queue.LocalQueuePartitionDiagnostics;
+import org.apache.nifi.controller.queue.QueueDiagnostics;
+import org.apache.nifi.controller.queue.QueueSize;
+import org.apache.nifi.controller.queue.RemoteQueuePartitionDiagnostics;
+import org.apache.nifi.controller.repository.FlowFileRecord;
+import org.apache.nifi.controller.repository.claim.ContentClaim;
+import org.apache.nifi.controller.repository.claim.ResourceClaim;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.state.SortedStateUtils;
+import org.apache.nifi.controller.status.ConnectionStatus;
+import org.apache.nifi.controller.status.PortStatus;
+import org.apache.nifi.controller.status.ProcessGroupStatus;
+import org.apache.nifi.controller.status.ProcessorStatus;
+import org.apache.nifi.controller.status.RemoteProcessGroupStatus;
+import org.apache.nifi.controller.status.history.GarbageCollectionHistory;
+import org.apache.nifi.controller.status.history.GarbageCollectionStatus;
+import org.apache.nifi.diagnostics.GarbageCollection;
+import org.apache.nifi.diagnostics.StorageUsage;
+import org.apache.nifi.diagnostics.SystemDiagnostics;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFilePrioritizer;
+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.NarClassLoadersHolder;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.provenance.lineage.ComputeLineageResult;
+import org.apache.nifi.provenance.lineage.ComputeLineageSubmission;
+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.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.DifferenceType;
+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;
+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;
+import org.apache.nifi.remote.RootGroupPort;
+import org.apache.nifi.reporting.Bulletin;
+import org.apache.nifi.reporting.BulletinRepository;
+import org.apache.nifi.reporting.ReportingTask;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.util.FlowDifferenceFilters;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.web.FlowModification;
+import org.apache.nifi.web.Revision;
+import org.apache.nifi.web.api.dto.action.ActionDTO;
+import org.apache.nifi.web.api.dto.action.HistoryDTO;
+import org.apache.nifi.web.api.dto.action.component.details.ComponentDetailsDTO;
+import org.apache.nifi.web.api.dto.action.component.details.ExtensionDetailsDTO;
+import org.apache.nifi.web.api.dto.action.component.details.RemoteProcessGroupDetailsDTO;
+import org.apache.nifi.web.api.dto.action.details.ActionDetailsDTO;
+import org.apache.nifi.web.api.dto.action.details.ConfigureDetailsDTO;
+import org.apache.nifi.web.api.dto.action.details.ConnectDetailsDTO;
+import org.apache.nifi.web.api.dto.action.details.MoveDetailsDTO;
+import org.apache.nifi.web.api.dto.action.details.PurgeDetailsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ClassLoaderDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ConnectionDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ConnectionDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ControllerServiceDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.GCDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.GarbageCollectionDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMControllerDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMFlowDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.JVMSystemDiagnosticsSnapshotDTO;
+import org.apache.nifi.web.api.dto.diagnostics.LocalQueuePartitionDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ProcessorDiagnosticsDTO;
+import org.apache.nifi.web.api.dto.diagnostics.RemoteQueuePartitionDTO;
+import org.apache.nifi.web.api.dto.diagnostics.RepositoryUsageDTO;
+import org.apache.nifi.web.api.dto.diagnostics.ThreadDumpDTO;
+import org.apache.nifi.web.api.dto.flow.FlowBreadcrumbDTO;
+import org.apache.nifi.web.api.dto.flow.FlowDTO;
+import org.apache.nifi.web.api.dto.flow.ProcessGroupFlowDTO;
+import org.apache.nifi.web.api.dto.provenance.lineage.LineageDTO;
+import org.apache.nifi.web.api.dto.provenance.lineage.LineageRequestDTO;
+import org.apache.nifi.web.api.dto.provenance.lineage.LineageRequestDTO.LineageRequestType;
+import org.apache.nifi.web.api.dto.provenance.lineage.LineageResultsDTO;
+import org.apache.nifi.web.api.dto.provenance.lineage.ProvenanceLinkDTO;
+import org.apache.nifi.web.api.dto.provenance.lineage.ProvenanceNodeDTO;
+import org.apache.nifi.web.api.dto.status.ConnectionStatusDTO;
+import org.apache.nifi.web.api.dto.status.ConnectionStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.PortStatusDTO;
+import org.apache.nifi.web.api.dto.status.PortStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
+import org.apache.nifi.web.api.dto.status.ProcessGroupStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO;
+import org.apache.nifi.web.api.dto.status.ProcessorStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusDTO;
+import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusSnapshotDTO;
+import org.apache.nifi.web.api.entity.AccessPolicyEntity;
+import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+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.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;
+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.net.UnknownHostException;
+import java.text.Collator;
+import java.text.NumberFormat;
+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 java.net.InetAddress;
+
+
+public final class DtoFactory {
+
+    @SuppressWarnings("rawtypes")
+    private final static Comparator<Class> CLASS_NAME_COMPARATOR = new Comparator<Class>() {
+        @Override
+        public int compare(final Class class1, final Class class2) {
+            return Collator.getInstance(Locale.US).compare(class1.getSimpleName(), class2.getSimpleName());
+        }
+    };
+    public static final String SENSITIVE_VALUE_MASK = "********";
+
+    private BulletinRepository bulletinRepository;
+    private ControllerServiceProvider controllerServiceProvider;
+    private EntityFactory entityFactory;
+    private Authorizer authorizer;
+    private ExtensionManager extensionManager;
+
+    public ControllerConfigurationDTO createControllerConfigurationDto(final ControllerFacade controllerFacade) {
+        final ControllerConfigurationDTO dto = new ControllerConfigurationDTO();
+        dto.setMaxTimerDrivenThreadCount(controllerFacade.getMaxTimerDrivenThreadCount());
+        dto.setMaxEventDrivenThreadCount(controllerFacade.getMaxEventDrivenThreadCount());
+        return dto;
+    }
+
+    public FlowConfigurationDTO createFlowConfigurationDto(final String autoRefreshInterval,
+                                                           final Long defaultBackPressureObjectThreshold,
+                                                           final String defaultBackPressureDataSizeThreshold,
+                                                           final String dcaeDistributorApiHostname) {
+        final FlowConfigurationDTO dto = new FlowConfigurationDTO();
+
+        // get the refresh interval
+        final long refreshInterval = FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.SECONDS);
+        dto.setAutoRefreshIntervalSeconds(refreshInterval);
+        dto.setSupportsManagedAuthorizer(AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer));
+        dto.setSupportsConfigurableUsersAndGroups(AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer));
+        dto.setSupportsConfigurableAuthorizer(AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer));
+
+        /* Renu - getting host IP */
+        dto.setDcaeDistributorApiHostname(dcaeDistributorApiHostname);
+
+        final Date now = new Date();
+        dto.setTimeOffset(TimeZone.getDefault().getOffset(now.getTime()));
+        dto.setCurrentTime(now);
+
+        dto.setDefaultBackPressureDataSizeThreshold(defaultBackPressureDataSizeThreshold);
+        dto.setDefaultBackPressureObjectThreshold(defaultBackPressureObjectThreshold);
+
+        return dto;
+    }
+
+    /**
+     * Creates an ActionDTO for the specified Action.
+     *
+     * @param action action
+     * @return dto
+     */
+    public ActionDTO createActionDto(final Action action) {
+        final ActionDTO actionDto = new ActionDTO();
+        actionDto.setId(action.getId());
+        actionDto.setSourceId(action.getSourceId());
+        actionDto.setSourceName(action.getSourceName());
+        actionDto.setSourceType(action.getSourceType().toString());
+        actionDto.setTimestamp(action.getTimestamp());
+        actionDto.setUserIdentity(action.getUserIdentity());
+        actionDto.setOperation(action.getOperation().toString());
+        actionDto.setActionDetails(createActionDetailsDto(action.getActionDetails()));
+        actionDto.setComponentDetails(createComponentDetailsDto(action.getComponentDetails()));
+
+        return actionDto;
+    }
+
+    /**
+     * Creates an ActionDetailsDTO for the specified ActionDetails.
+     *
+     * @param actionDetails details
+     * @return dto
+     */
+    private ActionDetailsDTO createActionDetailsDto(final ActionDetails actionDetails) {
+        if (actionDetails == null) {
+            return null;
+        }
+
+        if (actionDetails instanceof FlowChangeConfigureDetails) {
+            final ConfigureDetailsDTO configureDetails = new ConfigureDetailsDTO();
+            configureDetails.setName(((ConfigureDetails) actionDetails).getName());
+            configureDetails.setPreviousValue(((ConfigureDetails) actionDetails).getPreviousValue());
+            configureDetails.setValue(((ConfigureDetails) actionDetails).getValue());
+            return configureDetails;
+        } else if (actionDetails instanceof FlowChangeConnectDetails) {
+            final ConnectDetailsDTO connectDetails = new ConnectDetailsDTO();
+            connectDetails.setSourceId(((ConnectDetails) actionDetails).getSourceId());
+            connectDetails.setSourceName(((ConnectDetails) actionDetails).getSourceName());
+            connectDetails.setSourceType(((ConnectDetails) actionDetails).getSourceType().toString());
+            connectDetails.setRelationship(((ConnectDetails) actionDetails).getRelationship());
+            connectDetails.setDestinationId(((ConnectDetails) actionDetails).getDestinationId());
+            connectDetails.setDestinationName(((ConnectDetails) actionDetails).getDestinationName());
+            connectDetails.setDestinationType(((ConnectDetails) actionDetails).getDestinationType().toString());
+            return connectDetails;
+        } else if (actionDetails instanceof FlowChangeMoveDetails) {
+            final MoveDetailsDTO moveDetails = new MoveDetailsDTO();
+            moveDetails.setPreviousGroup(((MoveDetails) actionDetails).getPreviousGroup());
+            moveDetails.setPreviousGroupId(((MoveDetails) actionDetails).getPreviousGroupId());
+            moveDetails.setGroup(((MoveDetails) actionDetails).getGroup());
+            moveDetails.setGroupId(((MoveDetails) actionDetails).getGroupId());
+            return moveDetails;
+        } else if (actionDetails instanceof FlowChangePurgeDetails) {
+            final PurgeDetailsDTO purgeDetails = new PurgeDetailsDTO();
+            purgeDetails.setEndDate(((PurgeDetails) actionDetails).getEndDate());
+            return purgeDetails;
+        } else {
+            throw new WebApplicationException(new IllegalArgumentException(String.format("Unrecognized type of action details encountered %s during serialization.", actionDetails.toString())));
+        }
+    }
+
+    /**
+     * Creates a ComponentDetailsDTO for the specified ComponentDetails.
+     *
+     * @param componentDetails details
+     * @return dto
+     */
+    private ComponentDetailsDTO createComponentDetailsDto(final ComponentDetails componentDetails) {
+        if (componentDetails == null) {
+            return null;
+        }
+
+        if (componentDetails instanceof FlowChangeExtensionDetails) {
+            final ExtensionDetailsDTO processorDetails = new ExtensionDetailsDTO();
+            processorDetails.setType(((ExtensionDetails) componentDetails).getType());
+            return processorDetails;
+        } else if (componentDetails instanceof FlowChangeRemoteProcessGroupDetails) {
+            final RemoteProcessGroupDetailsDTO remoteProcessGroupDetails = new RemoteProcessGroupDetailsDTO();
+            remoteProcessGroupDetails.setUri(((RemoteProcessGroupDetails) componentDetails).getUri());
+            return remoteProcessGroupDetails;
+        } else {
+            throw new WebApplicationException(new IllegalArgumentException(String.format("Unrecognized type of component details encountered %s during serialization. ", componentDetails.toString())));
+        }
+    }
+
+    /**
+     * Creates a HistoryDTO from the specified History.
+     *
+     * @param history history
+     * @return dto
+     */
+    public HistoryDTO createHistoryDto(final History history) {
+        final HistoryDTO historyDto = new HistoryDTO();
+        historyDto.setTotal(history.getTotal());
+        historyDto.setLastRefreshed(history.getLastRefreshed());
+        return historyDto;
+    }
+
+    /**
+     * Creates a ComponentStateDTO for the given component and state's.
+     *
+     * @param componentId component id
+     * @param localState local state
+     * @param clusterState cluster state
+     * @return dto
+     */
+    public ComponentStateDTO createComponentStateDTO(final String componentId, final Class<?> componentClass, final StateMap localState, final StateMap clusterState) {
+        final ComponentStateDTO dto = new ComponentStateDTO();
+        dto.setComponentId(componentId);
+        dto.setStateDescription(getStateDescription(componentClass));
+        dto.setLocalState(createStateMapDTO(Scope.LOCAL, localState));
+        dto.setClusterState(createStateMapDTO(Scope.CLUSTER, clusterState));
+        return dto;
+    }
+
+    /**
+     * Gets the description of the state this component persists.
+     *
+     * @param componentClass the component class
+     * @return state description
+     */
+    private String getStateDescription(final Class<?> componentClass) {
+        final Stateful capabilityDesc = componentClass.getAnnotation(Stateful.class);
+        if (capabilityDesc != null) {
+            return capabilityDesc.description();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Creates a StateMapDTO for the given scope and state map.
+     *
+     * @param scope the scope
+     * @param stateMap the state map
+     * @return dto
+     */
+    public StateMapDTO createStateMapDTO(final Scope scope, final StateMap stateMap) {
+        if (stateMap == null) {
+            return null;
+        }
+
+        final StateMapDTO dto = new StateMapDTO();
+        dto.setScope(scope.toString());
+
+        final TreeMap<String, String> sortedState = new TreeMap<>(SortedStateUtils.getKeyComparator());
+        final Map<String, String> state = stateMap.toMap();
+        sortedState.putAll(state);
+
+        int count = 0;
+        final List<StateEntryDTO> stateEntries = new ArrayList<>();
+        final Set<Map.Entry<String, String>> entrySet = sortedState.entrySet();
+        for (final Iterator<Entry<String, String>> iter = entrySet.iterator(); iter.hasNext() && count++ < SortedStateUtils.MAX_COMPONENT_STATE_ENTRIES;) {
+            final Map.Entry<String, String> entry = iter.next();
+            final StateEntryDTO entryDTO = new StateEntryDTO();
+            entryDTO.setKey(entry.getKey());
+            entryDTO.setValue(entry.getValue());
+            stateEntries.add(entryDTO);
+        }
+        dto.setTotalEntryCount(state.size());
+        dto.setState(stateEntries);
+
+        return dto;
+    }
+
+    /**
+     * Creates CounterDTOs for each Counter specified.
+     *
+     * @param counterDtos dtos
+     * @return dto
+     */
+    public CountersSnapshotDTO createCountersDto(final Collection<CounterDTO> counterDtos) {
+        final CountersSnapshotDTO dto = new CountersSnapshotDTO();
+        dto.setCounters(counterDtos);
+        dto.setGenerated(new Date());
+        return dto;
+    }
+
+    /**
+     * Creates a CounterDTO from the specified Counter.
+     *
+     * @param counter counter
+     * @return dto
+     */
+    public CounterDTO createCounterDto(final Counter counter) {
+        final CounterDTO dto = new CounterDTO();
+        dto.setId(counter.getIdentifier());
+        dto.setContext(counter.getContext());
+        dto.setName(counter.getName());
+        dto.setValueCount(counter.getValue());
+        dto.setValue(FormatUtils.formatCount(counter.getValue()));
+        return dto;
+    }
+
+    /**
+     * Creates a PositionDTO from the specified position
+     *
+     * @param position position
+     * @return dto
+     */
+    public PositionDTO createPositionDto(final Position position) {
+        return new PositionDTO(position.getX(), position.getY());
+    }
+
+    private boolean isDropRequestComplete(final DropFlowFileState state) {
+        return DropFlowFileState.COMPLETE.equals(state) || DropFlowFileState.CANCELED.equals(state) || DropFlowFileState.FAILURE.equals(state);
+    }
+
+    /**
+     * Creates a DropRequestDTO from the specified flow file status.
+     *
+     * @param dropRequest dropRequest
+     * @return dto
+     */
+    public DropRequestDTO createDropRequestDTO(final DropFlowFileStatus dropRequest) {
+        final DropRequestDTO dto = new DropRequestDTO();
+        dto.setId(dropRequest.getRequestIdentifier());
+        dto.setSubmissionTime(new Date(dropRequest.getRequestSubmissionTime()));
+        dto.setLastUpdated(new Date(dropRequest.getLastUpdated()));
+        dto.setState(dropRequest.getState().toString());
+        dto.setFailureReason(dropRequest.getFailureReason());
+        dto.setFinished(isDropRequestComplete(dropRequest.getState()));
+
+        final QueueSize dropped = dropRequest.getDroppedSize();
+        dto.setDroppedCount(dropped.getObjectCount());
+        dto.setDroppedSize(dropped.getByteCount());
+        dto.setDropped(FormatUtils.formatCount(dropped.getObjectCount()) + " / " + FormatUtils.formatDataSize(dropped.getByteCount()));
+
+        final QueueSize current = dropRequest.getCurrentSize();
+        dto.setCurrentCount(current.getObjectCount());
+        dto.setCurrentSize(current.getByteCount());
+        dto.setCurrent(FormatUtils.formatCount(current.getObjectCount()) + " / " + FormatUtils.formatDataSize(current.getByteCount()));
+
+        final QueueSize original = dropRequest.getOriginalSize();
+        dto.setOriginalCount(original.getObjectCount());
+        dto.setOriginalSize(original.getByteCount());
+        dto.setOriginal(FormatUtils.formatCount(original.getObjectCount()) + " / " + FormatUtils.formatDataSize(original.getByteCount()));
+
+        if (isDropRequestComplete(dropRequest.getState())) {
+            dto.setPercentCompleted(100);
+        } else {
+            dto.setPercentCompleted((dropped.getObjectCount() * 100) / original.getObjectCount());
+        }
+
+        return dto;
+    }
+
+    private boolean isListingRequestComplete(final ListFlowFileState state) {
+        return ListFlowFileState.COMPLETE.equals(state) || ListFlowFileState.CANCELED.equals(state) || ListFlowFileState.FAILURE.equals(state);
+    }
+
+    private QueueSizeDTO createQueueSizeDTO(final QueueSize queueSize) {
+        final QueueSizeDTO dto = new QueueSizeDTO();
+        dto.setByteCount(queueSize.getByteCount());
+        dto.setObjectCount(queueSize.getObjectCount());
+        return dto;
+    }
+
+    /**
+     * Creates a ListingRequestDTO from the specified ListFlowFileStatus.
+     *
+     * @param listingRequest listingRequest
+     * @return dto
+     */
+    public ListingRequestDTO createListingRequestDTO(final ListFlowFileStatus listingRequest) {
+        final ListingRequestDTO dto = new ListingRequestDTO();
+        dto.setId(listingRequest.getRequestIdentifier());
+        dto.setSubmissionTime(new Date(listingRequest.getRequestSubmissionTime()));
+        dto.setLastUpdated(new Date(listingRequest.getLastUpdated()));
+        dto.setState(listingRequest.getState().toString());
+        dto.setFailureReason(listingRequest.getFailureReason());
+        dto.setFinished(isListingRequestComplete(listingRequest.getState()));
+        dto.setMaxResults(listingRequest.getMaxResults());
+        dto.setPercentCompleted(listingRequest.getCompletionPercentage());
+
+        dto.setQueueSize(createQueueSizeDTO(listingRequest.getQueueSize()));
+
+        if (isListingRequestComplete(listingRequest.getState())) {
+            final List<FlowFileSummary> flowFileSummaries = listingRequest.getFlowFileSummaries();
+            if (flowFileSummaries != null) {
+                final Date now = new Date();
+                final List<FlowFileSummaryDTO> summaryDtos = new ArrayList<>(flowFileSummaries.size());
+                for (final FlowFileSummary summary : flowFileSummaries) {
+                    summaryDtos.add(createFlowFileSummaryDTO(summary, now));
+                }
+                dto.setFlowFileSummaries(summaryDtos);
+            }
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a FlowFileSummaryDTO from the specified FlowFileSummary.
+     *
+     * @param summary summary
+     * @return dto
+     */
+    public FlowFileSummaryDTO createFlowFileSummaryDTO(final FlowFileSummary summary, final Date now) {
+        final FlowFileSummaryDTO dto = new FlowFileSummaryDTO();
+        dto.setUuid(summary.getUuid());
+        dto.setFilename(summary.getFilename());
+
+        dto.setPenalized(summary.isPenalized());
+        final long penaltyExpiration = summary.getPenaltyExpirationMillis() - now.getTime();
+        dto.setPenaltyExpiresIn(penaltyExpiration>=0?penaltyExpiration:0);
+
+        dto.setPosition(summary.getPosition());
+        dto.setSize(summary.getSize());
+
+        final long queuedDuration = now.getTime() - summary.getLastQueuedTime();
+        dto.setQueuedDuration(queuedDuration);
+
+        final long age = now.getTime() - summary.getLineageStartDate();
+        dto.setLineageDuration(age);
+
+        return dto;
+    }
+
+    /**
+     * Creates a FlowFileDTO from the specified FlowFileRecord.
+     *
+     * @param record record
+     * @return dto
+     */
+    public FlowFileDTO createFlowFileDTO(final FlowFileRecord record) {
+        final Date now = new Date();
+        final FlowFileDTO dto = new FlowFileDTO();
+        dto.setUuid(record.getAttribute(CoreAttributes.UUID.key()));
+        dto.setFilename(record.getAttribute(CoreAttributes.FILENAME.key()));
+
+        dto.setPenalized(record.isPenalized());
+        final long penaltyExpiration = record.getPenaltyExpirationMillis() - now.getTime();
+        dto.setPenaltyExpiresIn(penaltyExpiration>=0?penaltyExpiration:0);
+
+        dto.setSize(record.getSize());
+        dto.setAttributes(record.getAttributes());
+
+        final long queuedDuration = now.getTime() - record.getLastQueueDate();
+        dto.setQueuedDuration(queuedDuration);
+
+        final long age = now.getTime() - record.getLineageStartDate();
+        dto.setLineageDuration(age);
+
+        final ContentClaim contentClaim = record.getContentClaim();
+        if (contentClaim != null) {
+            final ResourceClaim resourceClaim = contentClaim.getResourceClaim();
+            dto.setContentClaimSection(resourceClaim.getSection());
+            dto.setContentClaimContainer(resourceClaim.getContainer());
+            dto.setContentClaimIdentifier(resourceClaim.getId());
+            dto.setContentClaimOffset(contentClaim.getOffset() + record.getContentClaimOffset());
+            dto.setContentClaimFileSizeBytes(record.getSize());
+            dto.setContentClaimFileSize(FormatUtils.formatDataSize(record.getSize()));
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a ConnectionDTO from the specified Connection.
+     *
+     * @param connection connection
+     * @return dto
+     */
+    public ConnectionDTO createConnectionDto(final Connection connection) {
+        if (connection == null) {
+            return null;
+        }
+
+        final ConnectionDTO dto = new ConnectionDTO();
+
+        dto.setId(connection.getIdentifier());
+        dto.setParentGroupId(connection.getProcessGroup().getIdentifier());
+
+        final List<PositionDTO> bendPoints = new ArrayList<>();
+        for (final Position bendPoint : connection.getBendPoints()) {
+            bendPoints.add(createPositionDto(bendPoint));
+        }
+        dto.setBends(bendPoints);
+        dto.setName(connection.getName());
+        dto.setLabelIndex(connection.getLabelIndex());
+        dto.setzIndex(connection.getZIndex());
+        dto.setSource(createConnectableDto(connection.getSource()));
+        dto.setDestination(createConnectableDto(connection.getDestination()));
+        dto.setVersionedComponentId(connection.getVersionedComponentId().orElse(null));
+
+        final FlowFileQueue flowFileQueue = connection.getFlowFileQueue();
+
+        dto.setBackPressureObjectThreshold(flowFileQueue.getBackPressureObjectThreshold());
+        dto.setBackPressureDataSizeThreshold(flowFileQueue.getBackPressureDataSizeThreshold());
+        dto.setFlowFileExpiration(flowFileQueue.getFlowFileExpiration());
+        dto.setPrioritizers(new ArrayList<String>());
+        for (final FlowFilePrioritizer comparator : flowFileQueue.getPriorities()) {
+            dto.getPrioritizers().add(comparator.getClass().getCanonicalName());
+        }
+
+        // For ports, we do not want to populate the relationships.
+        for (final Relationship selectedRelationship : connection.getRelationships()) {
+            if (!Relationship.ANONYMOUS.equals(selectedRelationship)) {
+                if (dto.getSelectedRelationships() == null) {
+                    dto.setSelectedRelationships(new TreeSet<String>(Collator.getInstance(Locale.US)));
+                }
+
+                dto.getSelectedRelationships().add(selectedRelationship.getName());
+            }
+        }
+
+        // For ports, we do not want to populate the relationships.
+        for (final Relationship availableRelationship : connection.getSource().getRelationships()) {
+            if (!Relationship.ANONYMOUS.equals(availableRelationship)) {
+                if (dto.getAvailableRelationships() == null) {
+                    dto.setAvailableRelationships(new TreeSet<String>(Collator.getInstance(Locale.US)));
+                }
+
+                dto.getAvailableRelationships().add(availableRelationship.getName());
+            }
+        }
+
+        final LoadBalanceStrategy loadBalanceStrategy = flowFileQueue.getLoadBalanceStrategy();
+        dto.setLoadBalancePartitionAttribute(flowFileQueue.getPartitioningAttribute());
+        dto.setLoadBalanceStrategy(loadBalanceStrategy.name());
+        dto.setLoadBalanceCompression(flowFileQueue.getLoadBalanceCompression().name());
+
+        if (loadBalanceStrategy == LoadBalanceStrategy.DO_NOT_LOAD_BALANCE) {
+            dto.setLoadBalanceStatus(ConnectionDTO.LOAD_BALANCE_NOT_CONFIGURED);
+        } else if (flowFileQueue.isActivelyLoadBalancing()) {
+            dto.setLoadBalanceStatus(ConnectionDTO.LOAD_BALANCE_ACTIVE);
+        } else {
+            dto.setLoadBalanceStatus(ConnectionDTO.LOAD_BALANCE_INACTIVE);
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a ConnectableDTO from the specified Connectable.
+     *
+     * @param connectable connectable
+     * @return dto
+     */
+    public ConnectableDTO createConnectableDto(final Connectable connectable) {
+        if (connectable == null) {
+            return null;
+        }
+
+        boolean isAuthorized = connectable.isAuthorized(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+
+        final ConnectableDTO dto = new ConnectableDTO();
+        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;
+            final RemoteProcessGroup remoteGroup = remoteGroupPort.getRemoteProcessGroup();
+            dto.setGroupId(remoteGroup.getIdentifier());
+            dto.setRunning(remoteGroupPort.isTargetRunning());
+            dto.setTransmitting(remoteGroupPort.isRunning());
+            dto.setExists(remoteGroupPort.getTargetExists());
+            if (isAuthorized) {
+                dto.setComments(remoteGroup.getComments());
+            }
+        } else {
+            dto.setGroupId(connectable.getProcessGroup().getIdentifier());
+            dto.setRunning(connectable.isRunning());
+            if (isAuthorized) {
+                dto.setComments(connectable.getComments());
+            }
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a LabelDTO from the specified Label.
+     *
+     * @param label label
+     * @return dto
+     */
+    public LabelDTO createLabelDto(final Label label) {
+        if (label == null) {
+            return null;
+        }
+
+        final LabelDTO dto = new LabelDTO();
+        dto.setId(label.getIdentifier());
+        dto.setPosition(createPositionDto(label.getPosition()));
+        dto.setStyle(label.getStyle());
+        dto.setHeight(label.getSize().getHeight());
+        dto.setWidth(label.getSize().getWidth());
+        dto.setLabel(label.getValue());
+        dto.setParentGroupId(label.getProcessGroup().getIdentifier());
+        dto.setVersionedComponentId(label.getVersionedComponentId().orElse(null));
+
+        return dto;
+    }
+
+    /**
+     * Creates a {@link UserDTO} from the specified {@link User}.
+     *
+     * @param user user
+     * @return dto
+     */
+    public UserDTO createUserDto(final User user, final Set<TenantEntity> groups, final Set<AccessPolicySummaryEntity> accessPolicies) {
+        if (user == null) {
+            return null;
+        }
+
+        final UserDTO dto = new UserDTO();
+        dto.setId(user.getIdentifier());
+        dto.setUserGroups(groups);
+        dto.setIdentity(user.getIdentity());
+        dto.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user));
+        dto.setAccessPolicies(accessPolicies);
+
+        return dto;
+    }
+
+    /**
+     * Creates a {@link TenantDTO} from the specified {@link User}.
+     *
+     * @param user user
+     * @return dto
+     */
+    public TenantDTO createTenantDTO(User user) {
+        if (user == null) {
+            return null;
+        }
+
+        final TenantDTO dto = new TenantDTO();
+        dto.setId(user.getIdentifier());
+        dto.setIdentity(user.getIdentity());
+        dto.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user));
+
+        return dto;
+    }
+
+    /**
+     * Creates a {@link UserGroupDTO} from the specified {@link Group}.
+     *
+     * @param userGroup user group
+     * @return dto
+     */
+    public UserGroupDTO createUserGroupDto(final Group userGroup, Set<TenantEntity> users, final Set<AccessPolicySummaryEntity> accessPolicies) {
+        if (userGroup == null) {
+            return null;
+        }
+
+        // convert to access policies to handle backward compatibility due to incorrect
+        // type in the UserGroupDTO
+        final Set<AccessPolicyEntity> policies = accessPolicies.stream().map(summaryEntity -> {
+            final AccessPolicyDTO policy = new AccessPolicyDTO();
+            policy.setId(summaryEntity.getId());
+
+            if (summaryEntity.getPermissions().getCanRead()) {
+                final AccessPolicySummaryDTO summary = summaryEntity.getComponent();
+                policy.setResource(summary.getResource());
+                policy.setAction(summary.getAction());
+                policy.setConfigurable(summary.getConfigurable());
+                policy.setComponentReference(summary.getComponentReference());
+            }
+
+            return entityFactory.createAccessPolicyEntity(policy, summaryEntity.getRevision(), summaryEntity.getPermissions());
+        }).collect(Collectors.toSet());
+
+        final UserGroupDTO dto = new UserGroupDTO();
+        dto.setId(userGroup.getIdentifier());
+        dto.setUsers(users);
+        dto.setIdentity(userGroup.getName());
+        dto.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, userGroup));
+        dto.setAccessPolicies(policies);
+
+        return dto;
+    }
+
+    /**
+     * Creates a {@link TenantDTO} from the specified {@link User}.
+     *
+     * @param userGroup user
+     * @return dto
+     */
+    public TenantDTO createTenantDTO(Group userGroup) {
+        if (userGroup == null) {
+            return null;
+        }
+
+        final TenantDTO dto = new TenantDTO();
+        dto.setId(userGroup.getIdentifier());
+        dto.setIdentity(userGroup.getName());
+        dto.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, userGroup));
+
+        return dto;
+    }
+
+    /**
+     * Creates a FunnelDTO from the specified Funnel.
+     *
+     * @param funnel funnel
+     * @return dto
+     */
+    public FunnelDTO createFunnelDto(final Funnel funnel) {
+        if (funnel == null) {
+            return null;
+        }
+
+        final FunnelDTO dto = new FunnelDTO();
+        dto.setId(funnel.getIdentifier());
+        dto.setPosition(createPositionDto(funnel.getPosition()));
+        dto.setParentGroupId(funnel.getProcessGroup().getIdentifier());
+        dto.setVersionedComponentId(funnel.getVersionedComponentId().orElse(null));
+
+        return dto;
+    }
+
+    /**
+     * Creates a SnippetDTO from the specified Snippet.
+     *
+     * @param snippet snippet
+     * @return dto
+     */
+    public SnippetDTO createSnippetDto(final Snippet snippet) {
+        final SnippetDTO dto = new SnippetDTO();
+        dto.setId(snippet.getId());
+        dto.setParentGroupId(snippet.getParentGroupId());
+
+        // populate the snippet contents ids
+        dto.setConnections(mapRevisionToDto(snippet.getConnections()));
+        dto.setFunnels(mapRevisionToDto(snippet.getFunnels()));
+        dto.setInputPorts(mapRevisionToDto(snippet.getInputPorts()));
+        dto.setLabels(mapRevisionToDto(snippet.getLabels()));
+        dto.setOutputPorts(mapRevisionToDto(snippet.getOutputPorts()));
+        dto.setProcessGroups(mapRevisionToDto(snippet.getProcessGroups()));
+        dto.setProcessors(mapRevisionToDto(snippet.getProcessors()));
+        dto.setRemoteProcessGroups(mapRevisionToDto(snippet.getRemoteProcessGroups()));
+
+        return dto;
+    }
+
+    private Map<String, RevisionDTO> mapRevisionToDto(final Map<String, Revision> revisionMap) {
+        final Map<String, RevisionDTO> dtos = new HashMap<>(revisionMap.size());
+        for (final Map.Entry<String, Revision> entry : revisionMap.entrySet()) {
+            final Revision revision = entry.getValue();
+            final RevisionDTO revisionDto = new RevisionDTO();
+            revisionDto.setClientId(revision.getClientId());
+            revisionDto.setVersion(revision.getVersion());
+
+            dtos.put(entry.getKey(), revisionDto);
+        }
+        return dtos;
+    }
+
+    /**
+     * Creates a TemplateDTO from the specified template.
+     *
+     * @param template template
+     * @return dto
+     */
+    public TemplateDTO createTemplateDTO(final Template template) {
+        if (template == null) {
+            return null;
+        }
+
+        final TemplateDTO original = template.getDetails();
+
+        final TemplateDTO copy = new TemplateDTO();
+        copy.setId(original.getId());
+        copy.setGroupId(template.getProcessGroup().getIdentifier());
+        copy.setName(original.getName());
+        copy.setDescription(original.getDescription());
+        copy.setTimestamp(original.getTimestamp());
+        copy.setUri(original.getUri());
+        copy.setEncodingVersion(original.getEncodingVersion());
+
+        return copy;
+    }
+
+
+    public RemoteProcessGroupStatusDTO createRemoteProcessGroupStatusDto(final RemoteProcessGroup remoteProcessGroup, final RemoteProcessGroupStatus remoteProcessGroupStatus) {
+        final RemoteProcessGroupStatusDTO dto = new RemoteProcessGroupStatusDTO();
+        dto.setId(remoteProcessGroupStatus.getId());
+        dto.setGroupId(remoteProcessGroupStatus.getGroupId());
+        dto.setTargetUri(remoteProcessGroupStatus.getTargetUri());
+        dto.setName(remoteProcessGroupStatus.getName());
+        dto.setTransmissionStatus(remoteProcessGroupStatus.getTransmissionStatus().toString());
+        dto.setStatsLastRefreshed(new Date());
+        dto.setValidationStatus(getRemoteProcessGroupValidationStatus(remoteProcessGroup).name());
+
+        final RemoteProcessGroupStatusSnapshotDTO snapshot = new RemoteProcessGroupStatusSnapshotDTO();
+        dto.setAggregateSnapshot(snapshot);
+
+        snapshot.setId(remoteProcessGroupStatus.getId());
+        snapshot.setGroupId(remoteProcessGroupStatus.getGroupId());
+        snapshot.setName(remoteProcessGroupStatus.getName());
+        snapshot.setTargetUri(remoteProcessGroupStatus.getTargetUri());
+        snapshot.setTransmissionStatus(remoteProcessGroupStatus.getTransmissionStatus().toString());
+
+        snapshot.setActiveThreadCount(remoteProcessGroupStatus.getActiveThreadCount());
+        snapshot.setFlowFilesSent(remoteProcessGroupStatus.getSentCount());
+        snapshot.setBytesSent(remoteProcessGroupStatus.getSentContentSize());
+        snapshot.setFlowFilesReceived(remoteProcessGroupStatus.getReceivedCount());
+        snapshot.setBytesReceived(remoteProcessGroupStatus.getReceivedContentSize());
+
+        StatusMerger.updatePrettyPrintedFields(snapshot);
+        return dto;
+    }
+
+    private ValidationStatus getRemoteProcessGroupValidationStatus(RemoteProcessGroup remoteProcessGroup) {
+        final boolean hasAuthIssue = remoteProcessGroup.getAuthorizationIssue() != null && !remoteProcessGroup.getAuthorizationIssue().isEmpty();
+        final Collection<ValidationResult> validationResults = remoteProcessGroup.validate();
+        final boolean hasValidationIssue = validationResults != null && !validationResults.isEmpty();
+        return hasAuthIssue || hasValidationIssue ? ValidationStatus.INVALID : ValidationStatus.VALID;
+    }
+
+    public ProcessGroupStatusDTO createConciseProcessGroupStatusDto(final ProcessGroupStatus processGroupStatus) {
+        final ProcessGroupStatusDTO processGroupStatusDto = new ProcessGroupStatusDTO();
+        processGroupStatusDto.setId(processGroupStatus.getId());
+        processGroupStatusDto.setName(processGroupStatus.getName());
+        processGroupStatusDto.setStatsLastRefreshed(new Date());
+
+        final ProcessGroupStatusSnapshotDTO snapshot = new ProcessGroupStatusSnapshotDTO();
+        processGroupStatusDto.setAggregateSnapshot(snapshot);
+
+        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());
+        snapshot.setBytesWritten(processGroupStatus.getBytesWritten());
+        snapshot.setFlowFilesIn(processGroupStatus.getInputCount());
+        snapshot.setBytesIn(processGroupStatus.getInputContentSize());
+        snapshot.setFlowFilesOut(processGroupStatus.getOutputCount());
+        snapshot.setBytesOut(processGroupStatus.getOutputContentSize());
+        snapshot.setFlowFilesTransferred(processGroupStatus.getFlowFilesTransferred());
+        snapshot.setBytesTransferred(processGroupStatus.getBytesTransferred());
+        snapshot.setFlowFilesSent(processGroupStatus.getFlowFilesSent());
+        snapshot.setBytesSent(processGroupStatus.getBytesSent());
+        snapshot.setFlowFilesReceived(processGroupStatus.getFlowFilesReceived());
+        snapshot.setBytesReceived(processGroupStatus.getBytesReceived());
+
+        snapshot.setActiveThreadCount(processGroupStatus.getActiveThreadCount());
+        snapshot.setTerminatedThreadCount(processGroupStatus.getTerminatedThreadCount());
+
+        StatusMerger.updatePrettyPrintedFields(snapshot);
+        return processGroupStatusDto;
+    }
+
+    public ProcessGroupStatusDTO createProcessGroupStatusDto(final ProcessGroup processGroup, final ProcessGroupStatus processGroupStatus) {
+        final ProcessGroupStatusDTO processGroupStatusDto = createConciseProcessGroupStatusDto(processGroupStatus);
+        final ProcessGroupStatusSnapshotDTO snapshot = processGroupStatusDto.getAggregateSnapshot();
+
+        // processor status
+        final Collection<ProcessorStatusSnapshotEntity> processorStatusSnapshotEntities = new ArrayList<>();
+        snapshot.setProcessorStatusSnapshots(processorStatusSnapshotEntities);
+        final Collection<ProcessorStatus> processorStatusCollection = processGroupStatus.getProcessorStatus();
+        if (processorStatusCollection != null) {
+            for (final ProcessorStatus processorStatus : processorStatusCollection) {
+                final ProcessorStatusDTO processorStatusDto = createProcessorStatusDto(processorStatus);
+                final ProcessorNode processor = processGroup.findProcessor(processorStatusDto.getId());
+                final PermissionsDTO processorPermissions = createPermissionsDto(processor);
+                processorStatusSnapshotEntities.add(entityFactory.createProcessorStatusSnapshotEntity(processorStatusDto.getAggregateSnapshot(), processorPermissions));
+            }
+        }
+
+        // connection status
+        final Collection<ConnectionStatusSnapshotEntity> connectionStatusDtoCollection = new ArrayList<>();
+        snapshot.setConnectionStatusSnapshots(connectionStatusDtoCollection);
+        final Collection<ConnectionStatus> connectionStatusCollection = processGroupStatus.getConnectionStatus();
+        if (connectionStatusCollection != null) {
+            for (final ConnectionStatus connectionStatus : connectionStatusCollection) {
+                final ConnectionStatusDTO connectionStatusDto = createConnectionStatusDto(connectionStatus);
+                final Connection connection = processGroup.findConnection(connectionStatusDto.getId());
+                final PermissionsDTO connectionPermissions = createPermissionsDto(connection);
+                connectionStatusDtoCollection.add(entityFactory.createConnectionStatusSnapshotEntity(connectionStatusDto.getAggregateSnapshot(), connectionPermissions));
+            }
+        }
+
+        // local child process groups
+        final Collection<ProcessGroupStatusSnapshotEntity> childProcessGroupStatusDtoCollection = new ArrayList<>();
+        snapshot.setProcessGroupStatusSnapshots(childProcessGroupStatusDtoCollection);
+        final Collection<ProcessGroupStatus> childProcessGroupStatusCollection = processGroupStatus.getProcessGroupStatus();
+        if (childProcessGroupStatusCollection != null) {
+            for (final ProcessGroupStatus childProcessGroupStatus : childProcessGroupStatusCollection) {
+                final ProcessGroupStatusDTO childProcessGroupStatusDto = createProcessGroupStatusDto(processGroup, childProcessGroupStatus);
+                final ProcessGroup childProcessGroup = processGroup.findProcessGroup(childProcessGroupStatusDto.getId());
+                final PermissionsDTO childProcessGroupPermissions = createPermissionsDto(childProcessGroup);
+                childProcessGroupStatusDtoCollection.add(entityFactory.createProcessGroupStatusSnapshotEntity(childProcessGroupStatusDto.getAggregateSnapshot(), childProcessGroupPermissions));
+            }
+        }
+
+        // remote child process groups
+        final Collection<RemoteProcessGroupStatusSnapshotEntity> childRemoteProcessGroupStatusDtoCollection = new ArrayList<>();
+        snapshot.setRemoteProcessGroupStatusSnapshots(childRemoteProcessGroupStatusDtoCollection);
+        final Collection<RemoteProcessGroupStatus> childRemoteProcessGroupStatusCollection = processGroupStatus.getRemoteProcessGroupStatus();
+        if (childRemoteProcessGroupStatusCollection != null) {
+            for (final RemoteProcessGroupStatus childRemoteProcessGroupStatus : childRemoteProcessGroupStatusCollection) {
+                final RemoteProcessGroup remoteProcessGroup = processGroup.findRemoteProcessGroup(childRemoteProcessGroupStatus.getId());
+                final RemoteProcessGroupStatusDTO childRemoteProcessGroupStatusDto = createRemoteProcessGroupStatusDto(remoteProcessGroup, childRemoteProcessGroupStatus);
+                final PermissionsDTO remoteProcessGroupPermissions = createPermissionsDto(remoteProcessGroup);
+                childRemoteProcessGroupStatusDtoCollection.add(entityFactory.createRemoteProcessGroupStatusSnapshotEntity(childRemoteProcessGroupStatusDto.getAggregateSnapshot(),
+                        remoteProcessGroupPermissions));
+            }
+        }
+
+        // input ports
+        final Collection<PortStatusSnapshotEntity> inputPortStatusDtoCollection = new ArrayList<>();
+        snapshot.setInputPortStatusSnapshots(inputPortStatusDtoCollection);
+        final Collection<PortStatus> inputPortStatusCollection = processGroupStatus.getInputPortStatus();
+        if (inputPortStatusCollection != null) {
+            for (final PortStatus portStatus : inputPortStatusCollection) {
+                final PortStatusDTO portStatusDto = createPortStatusDto(portStatus);
+                final Port inputPort = processGroup.findInputPort(portStatus.getId());
+                final PermissionsDTO inputPortPermissions = createPermissionsDto(inputPort);
+                inputPortStatusDtoCollection.add(entityFactory.createPortStatusSnapshotEntity(portStatusDto.getAggregateSnapshot(), inputPortPermissions));
+            }
+        }
+
+        // output ports
+        final Collection<PortStatusSnapshotEntity> outputPortStatusDtoCollection = new ArrayList<>();
+        snapshot.setOutputPortStatusSnapshots(outputPortStatusDtoCollection);
+        final Collection<PortStatus> outputPortStatusCollection = processGroupStatus.getOutputPortStatus();
+        if (outputPortStatusCollection != null) {
+            for (final PortStatus portStatus : outputPortStatusCollection) {
+                final PortStatusDTO portStatusDto = createPortStatusDto(portStatus);
+                final Port outputPort = processGroup.findOutputPort(portStatus.getId());
+                final PermissionsDTO outputPortPermissions = createPermissionsDto(outputPort);
+                outputPortStatusDtoCollection.add(entityFactory.createPortStatusSnapshotEntity(portStatusDto.getAggregateSnapshot(), outputPortPermissions));
+            }
+        }
+
+        return processGroupStatusDto;
+    }
+
+    public ConnectionStatusDTO createConnectionStatusDto(final ConnectionStatus connectionStatus) {
+        final ConnectionStatusDTO connectionStatusDto = new ConnectionStatusDTO();
+        connectionStatusDto.setGroupId(connectionStatus.getGroupId());
+        connectionStatusDto.setId(connectionStatus.getId());
+        connectionStatusDto.setName(connectionStatus.getName());
+        connectionStatusDto.setSourceId(connectionStatus.getSourceId());
+        connectionStatusDto.setSourceName(connectionStatus.getSourceName());
+        connectionStatusDto.setDestinationId(connectionStatus.getDestinationId());
+        connectionStatusDto.setDestinationName(connectionStatus.getDestinationName());
+        connectionStatusDto.setStatsLastRefreshed(new Date());
+
+        final ConnectionStatusSnapshotDTO snapshot = new ConnectionStatusSnapshotDTO();
+        connectionStatusDto.setAggregateSnapshot(snapshot);
+
+        snapshot.setId(connectionStatus.getId());
+        snapshot.setGroupId(connectionStatus.getGroupId());
+        snapshot.setName(connectionStatus.getName());
+        snapshot.setSourceName(connectionStatus.getSourceName());
+        snapshot.setDestinationName(connectionStatus.getDestinationName());
+
+        snapshot.setFlowFilesQueued(connectionStatus.getQueuedCount());
+        snapshot.setBytesQueued(connectionStatus.getQueuedBytes());
+
+        snapshot.setFlowFilesIn(connectionStatus.getInputCount());
+        snapshot.setBytesIn(connectionStatus.getInputBytes());
+
+        snapshot.setFlowFilesOut(connectionStatus.getOutputCount());
+        snapshot.setBytesOut(connectionStatus.getOutputBytes());
+
+        if (connectionStatus.getBackPressureObjectThreshold() > 0) {
+            snapshot.setPercentUseCount(Math.min(100, StatusMerger.getUtilization(connectionStatus.getQueuedCount(), connectionStatus.getBackPressureObjectThreshold())));
+        }
+        if (connectionStatus.getBackPressureBytesThreshold() > 0) {
+            snapshot.setPercentUseBytes(Math.min(100, StatusMerger.getUtilization(connectionStatus.getQueuedBytes(), connectionStatus.getBackPressureBytesThreshold())));
+        }
+
+        StatusMerger.updatePrettyPrintedFields(snapshot);
+
+        return connectionStatusDto;
+    }
+
+    public ProcessorStatusDTO createProcessorStatusDto(final ProcessorStatus procStatus) {
+        final ProcessorStatusDTO dto = new ProcessorStatusDTO();
+        dto.setId(procStatus.getId());
+        dto.setGroupId(procStatus.getGroupId());
+        dto.setName(procStatus.getName());
+        dto.setStatsLastRefreshed(new Date());
+        dto.setRunStatus(procStatus.getRunStatus().toString());
+
+        final ProcessorStatusSnapshotDTO snapshot = new ProcessorStatusSnapshotDTO();
+        dto.setAggregateSnapshot(snapshot);
+
+        snapshot.setId(procStatus.getId());
+        snapshot.setGroupId(procStatus.getGroupId());
+        snapshot.setName(procStatus.getName());
+
+        snapshot.setFlowFilesOut(procStatus.getOutputCount());
+        snapshot.setBytesOut(procStatus.getOutputBytes());
+
+        snapshot.setFlowFilesIn(procStatus.getInputCount());
+        snapshot.setBytesIn(procStatus.getInputBytes());
+
+        snapshot.setBytesRead(procStatus.getBytesRead());
+        snapshot.setBytesWritten(procStatus.getBytesWritten());
+
+        snapshot.setTaskCount(procStatus.getInvocations());
+        snapshot.setTasksDurationNanos(procStatus.getProcessingNanos());
+        snapshot.setTasksDuration(FormatUtils.formatHoursMinutesSeconds(procStatus.getProcessingNanos(), TimeUnit.NANOSECONDS));
+
+        // determine the run status
+        snapshot.setRunStatus(procStatus.getRunStatus().toString());
+        snapshot.setExecutionNode(procStatus.getExecutionNode().toString());
+
+        snapshot.setActiveThreadCount(procStatus.getActiveThreadCount());
+        snapshot.setTerminatedThreadCount(procStatus.getTerminatedThreadCount());
+        snapshot.setType(procStatus.getType());
+
+        StatusMerger.updatePrettyPrintedFields(snapshot);
+        return dto;
+    }
+
+    /**
+     * Creates a PortStatusDTO for the specified PortStatus.
+     *
+     * @param portStatus status
+     * @return dto
+     */
+    public PortStatusDTO createPortStatusDto(final PortStatus portStatus) {
+        final PortStatusDTO dto = new PortStatusDTO();
+        dto.setId(portStatus.getId());
+        dto.setGroupId(portStatus.getGroupId());
+        dto.setName(portStatus.getName());
+        dto.setRunStatus(portStatus.getRunStatus().toString());
+        dto.setTransmitting(portStatus.isTransmitting());
+        dto.setStatsLastRefreshed(new Date());
+
+        final PortStatusSnapshotDTO snapshot = new PortStatusSnapshotDTO();
+        dto.setAggregateSnapshot(snapshot);
+
+        snapshot.setId(portStatus.getId());
+        snapshot.setGroupId(portStatus.getGroupId());
+        snapshot.setName(portStatus.getName());
+        snapshot.setRunStatus(portStatus.getRunStatus().toString());
+
+        snapshot.setActiveThreadCount(portStatus.getActiveThreadCount());
+        snapshot.setFlowFilesOut(portStatus.getOutputCount());
+        snapshot.setBytesOut(portStatus.getOutputBytes());
+
+        snapshot.setFlowFilesIn(portStatus.getInputCount());
+        snapshot.setBytesIn(portStatus.getInputBytes());
+        StatusMerger.updatePrettyPrintedFields(snapshot);
+
+        return dto;
+    }
+
+    /**
+     * Copies the specified snippet.
+     *
+     * @param originalSnippet snippet
+     * @return dto
+     */
+    public FlowSnippetDTO copySnippetContents(final FlowSnippetDTO originalSnippet) {
+        final FlowSnippetDTO copySnippet = new FlowSnippetDTO();
+
+        if (originalSnippet.getConnections() != null) {
+            for (final ConnectionDTO connection : originalSnippet.getConnections()) {
+                copySnippet.getConnections().add(copy(connection));
+            }
+        }
+        if (originalSnippet.getInputPorts() != null) {
+            for (final PortDTO port : originalSnippet.getInputPorts()) {
+                copySnippet.getInputPorts().add(copy(port));
+            }
+        }
+        if (originalSnippet.getOutputPorts() != null) {
+            for (final PortDTO port : originalSnippet.getOutputPorts()) {
+                copySnippet.getOutputPorts().add(copy(port));
+            }
+        }
+        if (originalSnippet.getProcessGroups() != null) {
+            for (final ProcessGroupDTO processGroup : originalSnippet.getProcessGroups()) {
+                copySnippet.getProcessGroups().add(copy(processGroup, true));
+            }
+        }
+        if (originalSnippet.getProcessors() != null) {
+            for (final ProcessorDTO processor : originalSnippet.getProcessors()) {
+                copySnippet.getProcessors().add(copy(processor));
+            }
+        }
+        if (originalSnippet.getLabels() != null) {
+            for (final LabelDTO label : originalSnippet.getLabels()) {
+                copySnippet.getLabels().add(copy(label));
+            }
+        }
+        if (originalSnippet.getFunnels() != null) {
+            for (final FunnelDTO funnel : originalSnippet.getFunnels()) {
+                copySnippet.getFunnels().add(copy(funnel));
+            }
+        }
+        if (originalSnippet.getRemoteProcessGroups() != null) {
+            for (final RemoteProcessGroupDTO remoteGroup : originalSnippet.getRemoteProcessGroups()) {
+                copySnippet.getRemoteProcessGroups().add(copy(remoteGroup));
+            }
+        }
+        if (originalSnippet.getControllerServices() != null) {
+            for (final ControllerServiceDTO controllerService : originalSnippet.getControllerServices()) {
+                copySnippet.getControllerServices().add(copy(controllerService));
+            }
+        }
+
+        return copySnippet;
+    }
+
+    /**
+     * Creates a PortDTO from the specified Port.
+     *
+     * @param port port
+     * @return dto
+     */
+    public PortDTO createPortDto(final Port port) {
+        if (port == null) {
+            return null;
+        }
+
+        final PortDTO dto = new PortDTO();
+        dto.setId(port.getIdentifier());
+        dto.setPosition(createPositionDto(port.getPosition()));
+        dto.setName(port.getName());
+        dto.setComments(port.getComments());
+        dto.setConcurrentlySchedulableTaskCount(port.getMaxConcurrentTasks());
+        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) {
+            final RootGroupPort rootGroupPort = (RootGroupPort) port;
+            dto.setTransmitting(rootGroupPort.isTransmitting());
+            dto.setGroupAccessControl(rootGroupPort.getGroupAccessControl());
+            dto.setUserAccessControl(rootGroupPort.getUserAccessControl());
+        }
+
+        final Collection<ValidationResult> validationErrors = port.getValidationErrors();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        return dto;
+    }
+
+    public ReportingTaskDTO createReportingTaskDto(final ReportingTaskNode reportingTaskNode) {
+        final BundleCoordinate bundleCoordinate = reportingTaskNode.getBundleCoordinate();
+        final List<Bundle> compatibleBundles = extensionManager.getBundles(reportingTaskNode.getCanonicalClassName()).stream().filter(bundle -> {
+            final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate();
+            return bundleCoordinate.getGroup().equals(coordinate.getGroup()) && bundleCoordinate.getId().equals(coordinate.getId());
+        }).collect(Collectors.toList());
+
+        final ReportingTaskDTO dto = new ReportingTaskDTO();
+        dto.setId(reportingTaskNode.getIdentifier());
+        dto.setName(reportingTaskNode.getName());
+        dto.setType(reportingTaskNode.getCanonicalClassName());
+        dto.setBundle(createBundleDto(bundleCoordinate));
+        dto.setSchedulingStrategy(reportingTaskNode.getSchedulingStrategy().name());
+        dto.setSchedulingPeriod(reportingTaskNode.getSchedulingPeriod());
+        dto.setState(reportingTaskNode.getScheduledState().name());
+        dto.setActiveThreadCount(reportingTaskNode.getActiveThreadCount());
+        dto.setAnnotationData(reportingTaskNode.getAnnotationData());
+        dto.setComments(reportingTaskNode.getComments());
+        dto.setPersistsState(reportingTaskNode.getReportingTask().getClass().isAnnotationPresent(Stateful.class));
+        dto.setRestricted(reportingTaskNode.isRestricted());
+        dto.setDeprecated(reportingTaskNode.isDeprecated());
+        dto.setExtensionMissing(reportingTaskNode.isExtensionMissing());
+        dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+
+        final Map<String, String> defaultSchedulingPeriod = new HashMap<>();
+        defaultSchedulingPeriod.put(SchedulingStrategy.TIMER_DRIVEN.name(), SchedulingStrategy.TIMER_DRIVEN.getDefaultSchedulingPeriod());
+        defaultSchedulingPeriod.put(SchedulingStrategy.CRON_DRIVEN.name(), SchedulingStrategy.CRON_DRIVEN.getDefaultSchedulingPeriod());
+        dto.setDefaultSchedulingPeriod(defaultSchedulingPeriod);
+
+        // sort a copy of the properties
+        final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() {
+            @Override
+            public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) {
+                return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName());
+            }
+        });
+        sortedProperties.putAll(reportingTaskNode.getProperties());
+
+        // get the property order from the reporting task
+        final ReportingTask reportingTask = reportingTaskNode.getReportingTask();
+        final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>();
+        final List<PropertyDescriptor> descriptors = reportingTask.getPropertyDescriptors();
+        if (descriptors != null && !descriptors.isEmpty()) {
+            for (final PropertyDescriptor descriptor : descriptors) {
+                orderedProperties.put(descriptor, null);
+            }
+        }
+        orderedProperties.putAll(sortedProperties);
+
+        // build the descriptor and property dtos
+        dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>());
+        dto.setProperties(new LinkedHashMap<String, String>());
+        for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+
+            // store the property descriptor
+            dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, null));
+
+            // determine the property value - don't include sensitive properties
+            String propertyValue = entry.getValue();
+            if (propertyValue != null && descriptor.isSensitive()) {
+                propertyValue = SENSITIVE_VALUE_MASK;
+            }
+
+            // set the property value
+            dto.getProperties().put(descriptor.getName(), propertyValue);
+        }
+
+        final ValidationStatus validationStatus = reportingTaskNode.getValidationStatus(1, TimeUnit.MILLISECONDS);
+        dto.setValidationStatus(validationStatus.name());
+
+        // add the validation errors
+        final Collection<ValidationResult> validationErrors = reportingTaskNode.getValidationErrors();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        return dto;
+    }
+
+    public ControllerServiceDTO createControllerServiceDto(final ControllerServiceNode controllerServiceNode) {
+        final BundleCoordinate bundleCoordinate = controllerServiceNode.getBundleCoordinate();
+        final List<Bundle> compatibleBundles = extensionManager.getBundles(controllerServiceNode.getCanonicalClassName()).stream().filter(bundle -> {
+            final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate();
+            return bundleCoordinate.getGroup().equals(coordinate.getGroup()) && bundleCoordinate.getId().equals(coordinate.getId());
+        }).collect(Collectors.toList());
+
+        final ControllerServiceDTO dto = new ControllerServiceDTO();
+        dto.setId(controllerServiceNode.getIdentifier());
+        dto.setParentGroupId(controllerServiceNode.getProcessGroup() == null ? null : controllerServiceNode.getProcessGroup().getIdentifier());
+        dto.setName(controllerServiceNode.getName());
+        dto.setType(controllerServiceNode.getCanonicalClassName());
+        dto.setBundle(createBundleDto(bundleCoordinate));
+        dto.setControllerServiceApis(createControllerServiceApiDto(controllerServiceNode.getControllerServiceImplementation().getClass()));
+        dto.setState(controllerServiceNode.getState().name());
+        dto.setAnnotationData(controllerServiceNode.getAnnotationData());
+        dto.setComments(controllerServiceNode.getComments());
+        dto.setPersistsState(controllerServiceNode.getControllerServiceImplementation().getClass().isAnnotationPresent(Stateful.class));
+        dto.setRestricted(controllerServiceNode.isRestricted());
+        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>() {
+            @Override
+            public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) {
+                return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName());
+            }
+        });
+        sortedProperties.putAll(controllerServiceNode.getProperties());
+
+        // get the property order from the controller service
+        final ControllerService controllerService = controllerServiceNode.getControllerServiceImplementation();
+        final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>();
+        final List<PropertyDescriptor> descriptors = controllerService.getPropertyDescriptors();
+        if (descriptors != null && !descriptors.isEmpty()) {
+            for (final PropertyDescriptor descriptor : descriptors) {
+                orderedProperties.put(descriptor, null);
+            }
+        }
+        orderedProperties.putAll(sortedProperties);
+
+        // build the descriptor and property dtos
+        dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>());
+        dto.setProperties(new LinkedHashMap<String, String>());
+        for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+
+            // store the property descriptor
+            final String groupId = controllerServiceNode.getProcessGroup() == null ? null : controllerServiceNode.getProcessGroup().getIdentifier();
+            dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, groupId));
+
+            // determine the property value - don't include sensitive properties
+            String propertyValue = entry.getValue();
+            if (propertyValue != null && descriptor.isSensitive()) {
+                propertyValue = SENSITIVE_VALUE_MASK;
+            }
+
+            // set the property value
+            dto.getProperties().put(descriptor.getName(), propertyValue);
+        }
+
+        dto.setValidationStatus(controllerServiceNode.getValidationStatus(1, TimeUnit.MILLISECONDS).name());
+
+        // add the validation errors
+        final Collection<ValidationResult> validationErrors = controllerServiceNode.getValidationErrors();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        return dto;
+    }
+
+    public ControllerServiceReferencingComponentDTO createControllerServiceReferencingComponentDTO(final ComponentNode component) {
+        final ControllerServiceReferencingComponentDTO dto = new ControllerServiceReferencingComponentDTO();
+        dto.setId(component.getIdentifier());
+        dto.setName(component.getName());
+
+        String processGroupId = null;
+        List<PropertyDescriptor> propertyDescriptors = null;
+        Collection<ValidationResult> validationErrors = null;
+        if (component instanceof ProcessorNode) {
+            final ProcessorNode node = ((ProcessorNode) component);
+            dto.setGroupId(node.getProcessGroup().getIdentifier());
+            dto.setState(node.getScheduledState().name());
+            dto.setActiveThreadCount(node.getActiveThreadCount());
+            dto.setType(node.getComponentType());
+            dto.setReferenceType(Processor.class.getSimpleName());
+
+            propertyDescriptors = node.getProcessor().getPropertyDescriptors();
+            validationErrors = node.getValidationErrors();
+            processGroupId = node.getProcessGroup().getIdentifier();
+        } else if (component instanceof ControllerServiceNode) {
+            final ControllerServiceNode node = ((ControllerServiceNode) component);
+            dto.setState(node.getState().name());
+            dto.setType(node.getComponentType());
+            dto.setReferenceType(ControllerService.class.getSimpleName());
+
+            propertyDescriptors = node.getControllerServiceImplementation().getPropertyDescriptors();
+            validationErrors = node.getValidationErrors();
+            processGroupId = node.getProcessGroup() == null ? null : node.getProcessGroup().getIdentifier();
+        } else if (component instanceof ReportingTaskNode) {
+            final ReportingTaskNode node = ((ReportingTaskNode) component);
+            dto.setState(node.getScheduledState().name());
+            dto.setActiveThreadCount(node.getActiveThreadCount());
+            dto.setType(node.getComponentType());
+            dto.setReferenceType(ReportingTask.class.getSimpleName());
+
+            propertyDescriptors = node.getReportingTask().getPropertyDescriptors();
+            validationErrors = node.getValidationErrors();
+            processGroupId = null;
+        }
+
+        // ensure descriptors is non null
+        if (propertyDescriptors == null) {
+            propertyDescriptors = new ArrayList<>();
+        }
+
+        // process properties unconditionally since dynamic properties are available here and not in getPropertyDescriptors
+        final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() {
+            @Override
+            public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) {
+                return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName());
+            }
+        });
+        sortedProperties.putAll(component.getProperties());
+
+        final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>();
+        for (final PropertyDescriptor descriptor : propertyDescriptors) {
+            orderedProperties.put(descriptor, null);
+        }
+        orderedProperties.putAll(sortedProperties);
+
+        // build the descriptor and property dtos
+        dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>());
+        dto.setProperties(new LinkedHashMap<String, String>());
+        for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+
+            // store the property descriptor
+            dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, processGroupId));
+
+            // determine the property value - don't include sensitive properties
+            String propertyValue = entry.getValue();
+            if (propertyValue != null && descriptor.isSensitive()) {
+                propertyValue = SENSITIVE_VALUE_MASK;
+            }
+
+            // set the property value
+            dto.getProperties().put(descriptor.getName(), propertyValue);
+        }
+
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        return dto;
+    }
+
+    public RemoteProcessGroupPortDTO createRemoteProcessGroupPortDto(final RemoteGroupPort port) {
+        if (port == null) {
+            return null;
+        }
+
+        final RemoteProcessGroupPortDTO dto = new RemoteProcessGroupPortDTO();
+        dto.setId(port.getIdentifier());
+        dto.setGroupId(port.getRemoteProcessGroup().getIdentifier());
+        dto.setTargetId(port.getTargetIdentifier());
+        dto.setName(port.getName());
+        dto.setComments(port.getComments());
+        dto.setTransmitting(port.isRunning());
+        dto.setTargetRunning(port.isTargetRunning());
+        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());
+        batchDTO.setSize(port.getBatchSize());
+        batchDTO.setDuration(port.getBatchDuration());
+        dto.setBatchSettings(batchDTO);
+
+        // determine if this port is currently connected to another component locally
+        if (ConnectableType.REMOTE_OUTPUT_PORT.equals(port.getConnectableType())) {
+            dto.setConnected(!port.getConnections().isEmpty());
+        } else {
+            dto.setConnected(port.hasIncomingConnection());
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a RemoteProcessGroupDTO from the specified RemoteProcessGroup.
+     *
+     * @param group group
+     * @return dto
+     */
+    public RemoteProcessGroupDTO createRemoteProcessGroupDto(final RemoteProcessGroup group) {
+        if (group == null) {
+            return null;
+        }
+
+        final Set<RemoteProcessGroupPortDTO> inputPorts = new HashSet<>();
+        final Set<RemoteProcessGroupPortDTO> outputPorts = new HashSet<>();
+
+        int activeRemoteInputPortCount = 0;
+        int inactiveRemoteInputPortCount = 0;
+        for (final Port port : group.getInputPorts()) {
+            inputPorts.add(createRemoteProcessGroupPortDto((RemoteGroupPort) port));
+
+            if (port.hasIncomingConnection()) {
+                if (port.isRunning()) {
+                    activeRemoteInputPortCount++;
+                } else {
+                    inactiveRemoteInputPortCount++;
+                }
+            }
+        }
+
+        int activeRemoteOutputPortCount = 0;
+        int inactiveRemoteOutputPortCount = 0;
+        for (final Port port : group.getOutputPorts()) {
+            outputPorts.add(createRemoteProcessGroupPortDto((RemoteGroupPort) port));
+
+            if (!port.getConnections().isEmpty()) {
+                if (port.isRunning()) {
+                    activeRemoteOutputPortCount++;
+                } else {
+                    inactiveRemoteOutputPortCount++;
+                }
+            }
+        }
+
+        final RemoteProcessGroupContentsDTO contents = new RemoteProcessGroupContentsDTO();
+        contents.setInputPorts(inputPorts);
+        contents.setOutputPorts(outputPorts);
+
+        final RemoteProcessGroupDTO dto = new RemoteProcessGroupDTO();
+        dto.setId(group.getIdentifier());
+        dto.setName(group.getName());
+        dto.setPosition(createPositionDto(group.getPosition()));
+        dto.setComments(group.getComments());
+        dto.setTransmitting(group.isTransmitting());
+        dto.setCommunicationsTimeout(group.getCommunicationsTimeout());
+        dto.setYieldDuration(group.getYieldDuration());
+        dto.setParentGroupId(group.getProcessGroup().getIdentifier());
+        dto.setTargetUris(group.getTargetUris());
+        dto.setFlowRefreshed(group.getLastRefreshTime());
+        dto.setContents(contents);
+        dto.setTransportProtocol(group.getTransportProtocol().name());
+        dto.setProxyHost(group.getProxyHost());
+        dto.setProxyPort(group.getProxyPort());
+        dto.setProxyUser(group.getProxyUser());
+        if (!StringUtils.isEmpty(group.getProxyPassword())) {
+            dto.setProxyPassword(SENSITIVE_VALUE_MASK);
+        }
+
+        // only specify the secure flag if we know the target system has site to site enabled
+        if (group.isSiteToSiteEnabled()) {
+            dto.setTargetSecure(group.getSecureFlag());
+        }
+
+        if (group.getAuthorizationIssue() != null) {
+            dto.setAuthorizationIssues(Arrays.asList(group.getAuthorizationIssue()));
+        }
+
+        final Collection<ValidationResult> validationErrors = group.validate();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        dto.setLocalNetworkInterface(group.getNetworkInterface());
+
+        dto.setActiveRemoteInputPortCount(activeRemoteInputPortCount);
+        dto.setInactiveRemoteInputPortCount(inactiveRemoteInputPortCount);
+        dto.setActiveRemoteOutputPortCount(activeRemoteOutputPortCount);
+        dto.setInactiveRemoteOutputPortCount(inactiveRemoteOutputPortCount);
+        dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null));
+
+        final RemoteProcessGroupCounts counts = group.getCounts();
+        if (counts != null) {
+            dto.setInputPortCount(counts.getInputPortCount());
+            dto.setOutputPortCount(counts.getOutputPortCount());
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a FlowBreadcrumbEntity from the specified parent ProcessGroup.
+     *
+     * @param group group
+     * @return dto
+     */
+    private FlowBreadcrumbEntity createBreadcrumbEntity(final ProcessGroup group) {
+        if (group == null) {
+            return null;
+        }
+
+        final FlowBreadcrumbDTO dto = createBreadcrumbDto(group);
+        final PermissionsDTO permissions = createPermissionsDto(group);
+        final FlowBreadcrumbEntity entity = entityFactory.createFlowBreadcrumbEntity(dto, permissions);
+
+        if (group.getParent() != null) {
+            entity.setParentBreadcrumb(createBreadcrumbEntity(group.getParent()));
+        }
+
+        return entity;
+    }
+
+    /**
+     * Creates a FlowBreadcrumbDTO from the specified parent ProcessGroup.
+     *
+     * @param group group
+     * @return dto
+     */
+    private FlowBreadcrumbDTO createBreadcrumbDto(final ProcessGroup group) {
+        if (group == null) {
+            return null;
+        }
+
+        final FlowBreadcrumbDTO dto = new FlowBreadcrumbDTO();
+        dto.setId(group.getIdentifier());
+        dto.setName(group.getName());
+
+        final VersionControlInformationDTO versionControlInformation = createVersionControlInformationDto(group);
+        dto.setVersionControlInformation(versionControlInformation);
+
+        return dto;
+    }
+
+    public ComponentReferenceDTO createComponentReferenceDto(final Authorizable authorizable) {
+        if (authorizable == null || !(authorizable instanceof ComponentAuthorizable)) {
+            return null;
+        }
+
+        final ComponentAuthorizable componentAuthorizable = (ComponentAuthorizable) authorizable;
+        final ComponentReferenceDTO dto = new ComponentReferenceDTO();
+        dto.setId(componentAuthorizable.getIdentifier());
+        dto.setParentGroupId(componentAuthorizable.getProcessGroupIdentifier());
+        dto.setName(authorizable.getResource().getName());
+
+        return dto;
+    }
+
+    public AccessPolicySummaryDTO createAccessPolicySummaryDto(final AccessPolicy accessPolicy, final ComponentReferenceEntity componentReference) {
+        if (accessPolicy == null) {
+            return null;
+        }
+
+        final AccessPolicySummaryDTO dto = new AccessPolicySummaryDTO();
+        dto.setId(accessPolicy.getIdentifier());
+        dto.setResource(accessPolicy.getResource());
+        dto.setAction(accessPolicy.getAction().toString());
+        dto.setConfigurable(AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy));
+        dto.setComponentReference(componentReference);
+        return dto;
+    }
+
+    public AccessPolicyDTO createAccessPolicyDto(final AccessPolicy accessPolicy, final Set<TenantEntity> userGroups,
+                                                 final Set<TenantEntity> users, final ComponentReferenceEntity componentReference) {
+
+        if (accessPolicy == null) {
+            return null;
+        }
+
+        final AccessPolicyDTO dto = new AccessPolicyDTO();
+        dto.setUserGroups(userGroups);
+        dto.setUsers(users);
+        dto.setId(accessPolicy.getIdentifier());
+        dto.setResource(accessPolicy.getResource());
+        dto.setAction(accessPolicy.getAction().toString());
+        dto.setConfigurable(AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy));
+        dto.setComponentReference(componentReference);
+        return dto;
+    }
+
+    /**
+     * Creates the PermissionsDTO based on the specified Authorizable.
+     *
+     * @param authorizable authorizable
+     * @return dto
+     */
+    public PermissionsDTO createPermissionsDto(final Authorizable authorizable) {
+        return createPermissionsDto(authorizable, NiFiUserUtils.getNiFiUser());
+    }
+
+    /**
+     * Creates the PermissionsDTO based on the specified Authorizable for the given user
+     *
+     * @param authorizable authorizable
+     * @param user the NiFi User for which the Permissions are being created
+     * @return dto
+     */
+    public PermissionsDTO createPermissionsDto(final Authorizable authorizable, final NiFiUser user) {
+        final PermissionsDTO dto = new PermissionsDTO();
+        dto.setCanRead(authorizable.isAuthorized(authorizer, RequestAction.READ, user));
+        dto.setCanWrite(authorizable.isAuthorized(authorizer, RequestAction.WRITE, user));
+        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_PROCESSOR);
+        componentDto.setState(processorDto.getState());
+        componentDto.setValidationErrors(processorDto.getValidationErrors());
+        component.setComponent(componentDto);
+
+        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;
+        }
+
+        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 ComponentNode component) {
+        final AffectedComponentDTO dto = new AffectedComponentDTO();
+        dto.setId(component.getIdentifier());
+        dto.setName(component.getName());
+        dto.setProcessGroupId(component.getProcessGroupIdentifier());
+
+        if (component instanceof ProcessorNode) {
+            final ProcessorNode node = ((ProcessorNode) component);
+            dto.setState(node.getScheduledState().name());
+            dto.setActiveThreadCount(node.getActiveThreadCount());
+            dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
+        } else if (component instanceof ControllerServiceNode) {
+            final ControllerServiceNode node = ((ControllerServiceNode) component);
+            dto.setState(node.getState().name());
+            dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        }
+
+        final Collection<ValidationResult> validationErrors = component.getValidationErrors();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a ProcessGroupDTO from the specified ProcessGroup.
+     *
+     * @param group group
+     * @return dto
+     */
+    public ProcessGroupDTO createProcessGroupDto(final ProcessGroup group) {
+        return createProcessGroupDto(group, false);
+    }
+
+    public ProcessGroupFlowDTO createProcessGroupFlowDto(final ProcessGroup group, final ProcessGroupStatus groupStatus, final RevisionManager revisionManager,
+                                                         final Function<ProcessGroup, List<BulletinEntity>> getProcessGroupBulletins) {
+
+        final ProcessGroupFlowDTO dto = new ProcessGroupFlowDTO();
+        dto.setId(group.getIdentifier());
+        dto.setLastRefreshed(new Date());
+        dto.setBreadcrumb(createBreadcrumbEntity(group));
+        dto.setFlow(createFlowDto(group, groupStatus, revisionManager, getProcessGroupBulletins));
+
+        final ProcessGroup parent = group.getParent();
+        if (parent != null) {
+            dto.setParentGroupId(parent.getIdentifier());
+        }
+
+        return dto;
+    }
+
+    public FlowDTO createFlowDto(final ProcessGroup group, final ProcessGroupStatus groupStatus, final FlowSnippetDTO snippet, final RevisionManager revisionManager,
+                                 final Function<ProcessGroup, List<BulletinEntity>> getProcessGroupBulletins) {
+        if (snippet == null) {
+            return null;
+        }
+
+        final FlowDTO flow = new FlowDTO();
+
+        for (final ConnectionDTO snippetConnection : snippet.getConnections()) {
+            final Connection connection = group.getConnection(snippetConnection.getId());
+
+            // marshal the actual connection as the snippet is pruned
+            final ConnectionDTO dto = createConnectionDto(connection);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(connection.getIdentifier()));
+            final PermissionsDTO accessPolicy = createPermissionsDto(connection);
+            final ConnectionStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getConnectionStatus().stream().filter(connectionStatus -> connection.getIdentifier().equals(connectionStatus.getId())).findFirst().orElse(null),
+                    connectionStatus -> createConnectionStatusDto(connectionStatus)
+            );
+            flow.getConnections().add(entityFactory.createConnectionEntity(dto, revision, accessPolicy, status));
+        }
+
+        for (final FunnelDTO snippetFunnel : snippet.getFunnels()) {
+            final Funnel funnel = group.getFunnel(snippetFunnel.getId());
+
+            // marshal the actual funnel as the snippet is pruned
+            final FunnelDTO dto = createFunnelDto(funnel);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(funnel.getIdentifier()));
+            final PermissionsDTO accessPolicy = createPermissionsDto(funnel);
+            flow.getFunnels().add(entityFactory.createFunnelEntity(dto, revision, accessPolicy));
+        }
+
+        for (final PortDTO snippetInputPort : snippet.getInputPorts()) {
+            final Port inputPort = group.getInputPort(snippetInputPort.getId());
+
+            // marshal the actual port as the snippet is pruned
+            final PortDTO dto = createPortDto(inputPort);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(inputPort.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(inputPort);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(inputPort));
+            final PortStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getInputPortStatus().stream().filter(inputPortStatus -> inputPort.getIdentifier().equals(inputPortStatus.getId())).findFirst().orElse(null),
+                    inputPortStatus -> createPortStatusDto(inputPortStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPort.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            flow.getInputPorts().add(entityFactory.createPortEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        for (final PortDTO snippetOutputPort : snippet.getOutputPorts()) {
+            final Port outputPort = group.getOutputPort(snippetOutputPort.getId());
+
+            // marshal the actual port as the snippet is pruned
+            final PortDTO dto = createPortDto(outputPort);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(outputPort.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(outputPort);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(outputPort));
+            final PortStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getOutputPortStatus().stream().filter(outputPortStatus -> outputPort.getIdentifier().equals(outputPortStatus.getId())).findFirst().orElse(null),
+                    outputPortStatus -> createPortStatusDto(outputPortStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPort.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            flow.getOutputPorts().add(entityFactory.createPortEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        for (final LabelDTO snippetLabel : snippet.getLabels()) {
+            final Label label = group.getLabel(snippetLabel.getId());
+
+            // marshal the actual label as the snippet is pruned
+            final LabelDTO dto = createLabelDto(label);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(label.getIdentifier()));
+            final PermissionsDTO accessPolicy = createPermissionsDto(label);
+            flow.getLabels().add(entityFactory.createLabelEntity(dto, revision, accessPolicy));
+        }
+
+        for (final ProcessGroupDTO snippetProcessGroup : snippet.getProcessGroups()) {
+            final ProcessGroup processGroup = group.getProcessGroup(snippetProcessGroup.getId());
+
+            // marshal the actual group as the snippet is pruned
+            final ProcessGroupDTO dto = createProcessGroupDto(processGroup);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(processGroup);
+            final ProcessGroupStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getProcessGroupStatus().stream().filter(processGroupStatus -> processGroup.getIdentifier().equals(processGroupStatus.getId())).findFirst().orElse(null),
+                    processGroupStatus -> createConciseProcessGroupStatusDto(processGroupStatus)
+            );
+            final List<BulletinEntity> bulletins = getProcessGroupBulletins.apply(processGroup);
+            flow.getProcessGroups().add(entityFactory.createProcessGroupEntity(dto, revision, permissions, status, bulletins));
+        }
+
+        for (final ProcessorDTO snippetProcessor : snippet.getProcessors()) {
+            final ProcessorNode processor = group.getProcessor(snippetProcessor.getId());
+
+            // marshal the actual processor as the snippet is pruned
+            final ProcessorDTO dto = createProcessorDto(processor);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(processor.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(processor);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(processor));
+            final ProcessorStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getProcessorStatus().stream().filter(processorStatus -> processor.getIdentifier().equals(processorStatus.getId())).findFirst().orElse(null),
+                    processorStatus -> createProcessorStatusDto(processorStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(processor.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            flow.getProcessors().add(entityFactory.createProcessorEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        for (final RemoteProcessGroupDTO snippetRemoteProcessGroup : snippet.getRemoteProcessGroups()) {
+            final RemoteProcessGroup remoteProcessGroup = group.getRemoteProcessGroup(snippetRemoteProcessGroup.getId());
+
+            // marshal the actual rpm as the snippet is pruned
+            final RemoteProcessGroupDTO dto = createRemoteProcessGroupDto(remoteProcessGroup);
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(remoteProcessGroup.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(remoteProcessGroup);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(remoteProcessGroup));
+            final RemoteProcessGroupStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getRemoteProcessGroupStatus().stream().filter(rpgStatus -> remoteProcessGroup.getIdentifier().equals(rpgStatus.getId())).findFirst().orElse(null),
+                    remoteProcessGroupStatus -> createRemoteProcessGroupStatusDto(remoteProcessGroup, remoteProcessGroupStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroup.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            flow.getRemoteProcessGroups().add(entityFactory.createRemoteProcessGroupEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        return flow;
+    }
+
+    private <T, S> T getComponentStatus(final Supplier<S> getComponentStatus, final Function<S, T> convertToDto) {
+        final T statusDTO;
+        final S status = getComponentStatus.get();
+        if (status != null) {
+            statusDTO = convertToDto.apply(status);
+        } else {
+            statusDTO = null;
+        }
+        return statusDTO;
+    }
+
+    public FlowDTO createFlowDto(final ProcessGroup group, final ProcessGroupStatus groupStatus, final RevisionManager revisionManager,
+                                 final Function<ProcessGroup, List<BulletinEntity>> getProcessGroupBulletins) {
+        final FlowDTO dto = new FlowDTO();
+
+        for (final ProcessorNode procNode : group.getProcessors()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(procNode.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(procNode);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(procNode));
+            final ProcessorStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getProcessorStatus().stream().filter(processorStatus -> procNode.getIdentifier().equals(processorStatus.getId())).findFirst().orElse(null),
+                    processorStatus -> createProcessorStatusDto(processorStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(procNode.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            dto.getProcessors().add(entityFactory.createProcessorEntity(createProcessorDto(procNode), revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        for (final Connection connNode : group.getConnections()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(connNode.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(connNode);
+            final ConnectionStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getConnectionStatus().stream().filter(connectionStatus -> connNode.getIdentifier().equals(connectionStatus.getId())).findFirst().orElse(null),
+                    connectionStatus -> createConnectionStatusDto(connectionStatus)
+            );
+            dto.getConnections().add(entityFactory.createConnectionEntity(createConnectionDto(connNode), revision, permissions, status));
+        }
+
+        for (final Label label : group.getLabels()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(label.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(label);
+            dto.getLabels().add(entityFactory.createLabelEntity(createLabelDto(label), revision, permissions));
+        }
+
+        for (final Funnel funnel : group.getFunnels()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(funnel.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(funnel);
+            dto.getFunnels().add(entityFactory.createFunnelEntity(createFunnelDto(funnel), revision, permissions));
+        }
+
+        for (final ProcessGroup childGroup : group.getProcessGroups()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(childGroup.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(childGroup);
+            final ProcessGroupStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getProcessGroupStatus().stream().filter(processGroupStatus -> childGroup.getIdentifier().equals(processGroupStatus.getId())).findFirst().orElse(null),
+                    processGroupStatus -> createConciseProcessGroupStatusDto(processGroupStatus)
+            );
+            final List<BulletinEntity> bulletins = getProcessGroupBulletins.apply(childGroup);
+            dto.getProcessGroups().add(entityFactory.createProcessGroupEntity(createProcessGroupDto(childGroup), revision, permissions, status, bulletins));
+        }
+
+        for (final RemoteProcessGroup rpg : group.getRemoteProcessGroups()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(rpg.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(rpg);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(rpg));
+            final RemoteProcessGroupStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getRemoteProcessGroupStatus().stream().filter(remoteProcessGroupStatus -> rpg.getIdentifier().equals(remoteProcessGroupStatus.getId())).findFirst().orElse(null),
+                    remoteProcessGroupStatus -> createRemoteProcessGroupStatusDto(rpg, remoteProcessGroupStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(rpg.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            dto.getRemoteProcessGroups().add(entityFactory.createRemoteProcessGroupEntity(createRemoteProcessGroupDto(rpg), revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        for (final Port inputPort : group.getInputPorts()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(inputPort.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(inputPort);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(inputPort));
+            final PortStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getInputPortStatus().stream().filter(inputPortStatus -> inputPort.getIdentifier().equals(inputPortStatus.getId())).findFirst().orElse(null),
+                    inputPortStatus -> createPortStatusDto(inputPortStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPort.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            dto.getInputPorts().add(entityFactory.createPortEntity(createPortDto(inputPort), revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        for (final Port outputPort : group.getOutputPorts()) {
+            final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(outputPort.getIdentifier()));
+            final PermissionsDTO permissions = createPermissionsDto(outputPort);
+            final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(outputPort));
+            final PortStatusDTO status = getComponentStatus(
+                    () -> groupStatus.getOutputPortStatus().stream().filter(outputPortStatus -> outputPort.getIdentifier().equals(outputPortStatus.getId())).findFirst().orElse(null),
+                    outputPortStatus -> createPortStatusDto(outputPortStatus)
+            );
+            final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPort.getIdentifier()));
+            final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
+            dto.getOutputPorts().add(entityFactory.createPortEntity(createPortDto(outputPort), revision, permissions, operatePermissions, status, bulletinEntities));
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a ProcessGroupDTO from the specified ProcessGroup.
+     *
+     * @param group group
+     * @param recurse recurse
+     * @return dto
+     */
+    public ProcessGroupDTO createProcessGroupDto(final ProcessGroup group, final boolean recurse) {
+        final ProcessGroupDTO dto = createConciseProcessGroupDto(group);
+        dto.setContents(createProcessGroupContentsDto(group, recurse));
+        return dto;
+    }
+
+    /**
+     * Creates a ProcessGroupDTO from the specified ProcessGroup.
+     *
+     * @param group group
+     * @return dto
+     */
+    private ProcessGroupDTO createConciseProcessGroupDto(final ProcessGroup group) {
+        if (group == null) {
+            return null;
+        }
+
+        final ProcessGroupDTO dto = new ProcessGroupDTO();
+        dto.setId(group.getIdentifier());
+        dto.setPosition(createPositionDto(group.getPosition()));
+        dto.setComments(group.getComments());
+        dto.setName(group.getName());
+        dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null));
+        dto.setVersionControlInformation(createVersionControlInformationDto(group));
+
+        final Map<String, String> variables = group.getVariableRegistry().getVariableMap().entrySet().stream()
+                .collect(Collectors.toMap(entry -> entry.getKey().getName(), entry -> entry.getValue()));
+        dto.setVariables(variables);
+
+        final ProcessGroup parentGroup = group.getParent();
+        if (parentGroup != null) {
+            dto.setParentGroupId(parentGroup.getIdentifier());
+        }
+
+        final ProcessGroupCounts counts = group.getCounts();
+        dto.setRunningCount(counts.getRunningCount());
+        dto.setStoppedCount(counts.getStoppedCount());
+        dto.setInvalidCount(counts.getInvalidCount());
+        dto.setDisabledCount(counts.getDisabledCount());
+        dto.setInputPortCount(counts.getInputPortCount());
+        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;
+    }
+
+
+    public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison) {
+        final Map<ComponentDifferenceDTO, List<DifferenceDTO>> differencesByComponent = new HashMap<>();
+
+        for (final FlowDifference difference : comparison.getDifferences()) {
+            // Ignore these as local differences for now because we can't do anything with it
+            if (difference.getDifferenceType() == DifferenceType.BUNDLE_CHANGED) {
+                continue;
+            }
+
+            // Ignore differences for adding remote ports
+            if (FlowDifferenceFilters.isAddedOrRemovedRemotePort(difference)) {
+                continue;
+            }
+
+            if (FlowDifferenceFilters.isIgnorableVersionedFlowCoordinateChange(difference)) {
+                continue;
+            }
+
+            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());
+
+            differences.add(dto);
+        }
+
+        for (final Map.Entry<ComponentDifferenceDTO, List<DifferenceDTO>> entry : differencesByComponent.entrySet()) {
+            entry.getKey().setDifferences(entry.getValue());
+        }
+
+        return differencesByComponent.keySet();
+    }
+
+    private ComponentDifferenceDTO createComponentDifference(final FlowDifference difference) {
+        VersionedComponent component = difference.getComponentA();
+        if (component == null || difference.getComponentB() instanceof InstantiatedVersionedComponent) {
+            component = difference.getComponentB();
+        }
+
+        final ComponentDifferenceDTO dto = new ComponentDifferenceDTO();
+        dto.setComponentName(component.getName());
+        dto.setComponentType(component.getComponentType().toString());
+
+        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;
+    }
+
+
+    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.setRegistryName(versionControlInfo.getRegistryName());
+        dto.setBucketId(versionControlInfo.getBucketIdentifier());
+        dto.setBucketName(versionControlInfo.getBucketName());
+        dto.setFlowId(versionControlInfo.getFlowIdentifier());
+        dto.setFlowName(versionControlInfo.getFlowName());
+        dto.setFlowDescription(versionControlInfo.getFlowDescription());
+        dto.setVersion(versionControlInfo.getVersion());
+
+        final VersionedFlowStatus status = versionControlInfo.getStatus();
+        final VersionedFlowState state = status.getState();
+        dto.setState(state == null ? null : state.name());
+        dto.setStateExplanation(status.getStateExplanation());
+
+        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.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()));
+        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());
+
+                    if (rpg.getInputPorts() != null) {
+                        rpg.getInputPorts().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()
+                .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.
+     *
+     * @param group group
+     * @param recurse recurse
+     * @return dto
+     */
+    private FlowSnippetDTO createProcessGroupContentsDto(final ProcessGroup group, final boolean recurse) {
+        if (group == null) {
+            return null;
+        }
+
+        final FlowSnippetDTO dto = new FlowSnippetDTO();
+
+        for (final ProcessorNode procNode : group.getProcessors()) {
+            dto.getProcessors().add(createProcessorDto(procNode));
+        }
+
+        for (final Connection connNode : group.getConnections()) {
+            dto.getConnections().add(createConnectionDto(connNode));
+        }
+
+        for (final Label label : group.getLabels()) {
+            dto.getLabels().add(createLabelDto(label));
+        }
+
+        for (final Funnel funnel : group.getFunnels()) {
+            dto.getFunnels().add(createFunnelDto(funnel));
+        }
+
+        for (final ProcessGroup childGroup : group.getProcessGroups()) {
+            if (recurse) {
+                dto.getProcessGroups().add(createProcessGroupDto(childGroup, recurse));
+            } else {
+                dto.getProcessGroups().add(createConciseProcessGroupDto(childGroup));
+            }
+        }
+
+        for (final RemoteProcessGroup remoteProcessGroup : group.getRemoteProcessGroups()) {
+            dto.getRemoteProcessGroups().add(createRemoteProcessGroupDto(remoteProcessGroup));
+        }
+
+        for (final Port inputPort : group.getInputPorts()) {
+            dto.getInputPorts().add(createPortDto(inputPort));
+        }
+
+        for (final Port outputPort : group.getOutputPorts()) {
+            dto.getOutputPorts().add(createPortDto(outputPort));
+        }
+
+        return dto;
+    }
+
+    private boolean isRestricted(final Class<?> cls) {
+        return cls.isAnnotationPresent(Restricted.class);
+    }
+
+    private String getUsageRestriction(final Class<?> cls) {
+        final Restricted restricted = cls.getAnnotation(Restricted.class);
+
+        if (restricted == null) {
+            return null;
+        }
+
+        if (StringUtils.isBlank(restricted.value())) {
+            return null;
+        }
+
+        return restricted.value();
+    }
+
+    private Set<ExplicitRestrictionDTO> getExplicitRestrictions(final Class<?> cls) {
+        final Restricted restricted = cls.getAnnotation(Restricted.class);
+
+        if (restricted == null) {
+            return null;
+        }
+
+        final Restriction[] restrictions = restricted.restrictions();
+
+        if (restrictions == null || restrictions.length == 0) {
+            return null;
+        }
+
+        return Arrays.stream(restrictions).map(restriction -> {
+            final RequiredPermissionDTO requiredPermission = new RequiredPermissionDTO();
+            requiredPermission.setId(restriction.requiredPermission().getPermissionIdentifier());
+            requiredPermission.setLabel(restriction.requiredPermission().getPermissionLabel());
+
+            final ExplicitRestrictionDTO usageRestriction = new ExplicitRestrictionDTO();
+            usageRestriction.setRequiredPermission(requiredPermission);
+            usageRestriction.setExplanation(restriction.explanation());
+            return usageRestriction;
+        }).collect(Collectors.toSet());
+    }
+
+    private String getDeprecationReason(final Class<?> cls) {
+        final DeprecationNotice deprecationNotice = cls.getAnnotation(DeprecationNotice.class);
+        return deprecationNotice == null ? null : deprecationNotice.reason();
+    }
+
+    public Set<AffectedComponentEntity> createAffectedComponentEntities(final Set<ComponentNode> affectedComponents, final RevisionManager revisionManager) {
+        return affectedComponents.stream()
+                .map(component -> {
+                    final AffectedComponentDTO affectedComponent = createAffectedComponentDto(component);
+                    final PermissionsDTO permissions = createPermissionsDto(component);
+                    final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(component.getIdentifier()));
+                    return entityFactory.createAffectedComponentEntity(affectedComponent, revision, permissions);
+                })
+                .collect(Collectors.toSet());
+    }
+
+    public VariableRegistryDTO createVariableRegistryDto(final ProcessGroup processGroup, final RevisionManager revisionManager) {
+        final ComponentVariableRegistry variableRegistry = processGroup.getVariableRegistry();
+
+        final List<String> variableNames = variableRegistry.getVariableMap().keySet().stream()
+                .map(descriptor -> descriptor.getName())
+                .collect(Collectors.toList());
+
+        final Set<VariableEntity> variableEntities = new LinkedHashSet<>();
+
+        for (final String variableName : variableNames) {
+            final VariableDTO variableDto = new VariableDTO();
+            variableDto.setName(variableName);
+            variableDto.setValue(variableRegistry.getVariableValue(variableName));
+            variableDto.setProcessGroupId(processGroup.getIdentifier());
+
+            final Set<AffectedComponentEntity> affectedComponentEntities = createAffectedComponentEntities(processGroup.getComponentsAffectedByVariable(variableName), revisionManager);
+
+            boolean canWrite = true;
+            for (final AffectedComponentEntity affectedComponent : affectedComponentEntities) {
+                final PermissionsDTO permissions = affectedComponent.getPermissions();
+                if (!permissions.getCanRead() || !permissions.getCanWrite()) {
+                    canWrite = false;
+                    break;
+                }
+            }
+
+            variableDto.setAffectedComponents(affectedComponentEntities);
+
+            final VariableEntity variableEntity = new VariableEntity();
+            variableEntity.setVariable(variableDto);
+            variableEntity.setCanWrite(canWrite);
+
+            variableEntities.add(variableEntity);
+        }
+
+        final VariableRegistryDTO registryDto = new VariableRegistryDTO();
+        registryDto.setProcessGroupId(processGroup.getIdentifier());
+        registryDto.setVariables(variableEntities);
+
+        return registryDto;
+    }
+
+    public VariableRegistryUpdateRequestDTO createVariableRegistryUpdateRequestDto(final VariableRegistryUpdateRequest request) {
+        final VariableRegistryUpdateRequestDTO dto = new VariableRegistryUpdateRequestDTO();
+        dto.setComplete(request.isComplete());
+        dto.setFailureReason(request.getFailureReason());
+        dto.setLastUpdated(request.getLastUpdated());
+        dto.setProcessGroupId(request.getProcessGroupId());
+        dto.setRequestId(request.getRequestId());
+        dto.setSubmissionTime(request.getSubmissionTime());
+
+        final List<VariableRegistryUpdateStepDTO> updateSteps = new ArrayList<>();
+        updateSteps.add(createVariableRegistryUpdateStepDto(request.getIdentifyRelevantComponentsStep()));
+        updateSteps.add(createVariableRegistryUpdateStepDto(request.getStopProcessorsStep()));
+        updateSteps.add(createVariableRegistryUpdateStepDto(request.getDisableServicesStep()));
+        updateSteps.add(createVariableRegistryUpdateStepDto(request.getApplyUpdatesStep()));
+        updateSteps.add(createVariableRegistryUpdateStepDto(request.getEnableServicesStep()));
+        updateSteps.add(createVariableRegistryUpdateStepDto(request.getStartProcessorsStep()));
+        dto.setUpdateSteps(updateSteps);
+
+        dto.setAffectedComponents(new HashSet<>(request.getAffectedComponents().values()));
+
+        return dto;
+    }
+
+    public VariableRegistryUpdateStepDTO createVariableRegistryUpdateStepDto(final VariableRegistryUpdateStep step) {
+        final VariableRegistryUpdateStepDTO dto = new VariableRegistryUpdateStepDTO();
+        dto.setComplete(step.isComplete());
+        dto.setDescription(step.getDescription());
+        dto.setFailureReason(step.getFailureReason());
+        return dto;
+    }
+
+
+    public VariableRegistryDTO populateAffectedComponents(final VariableRegistryDTO variableRegistry, final ProcessGroup group, final RevisionManager revisionManager) {
+        if (!group.getIdentifier().equals(variableRegistry.getProcessGroupId())) {
+            throw new IllegalArgumentException("Variable Registry does not have the same Group ID as the given Process Group");
+        }
+
+        final Set<VariableEntity> variableEntities = new LinkedHashSet<>();
+
+        if (variableRegistry.getVariables() != null) {
+            for (final VariableEntity inputEntity : variableRegistry.getVariables()) {
+                final VariableEntity entity = new VariableEntity();
+
+                final VariableDTO inputDto = inputEntity.getVariable();
+                final VariableDTO variableDto = new VariableDTO();
+                variableDto.setName(inputDto.getName());
+                variableDto.setValue(inputDto.getValue());
+                variableDto.setProcessGroupId(group.getIdentifier());
+
+                final Set<AffectedComponentEntity> affectedComponentEntities = createAffectedComponentEntities(group.getComponentsAffectedByVariable(variableDto.getName()), revisionManager);
+
+                boolean canWrite = true;
+                for (final AffectedComponentEntity affectedComponent : affectedComponentEntities) {
+                    final PermissionsDTO permissions = affectedComponent.getPermissions();
+                    if (!permissions.getCanRead() || !permissions.getCanWrite()) {
+                        canWrite = false;
+                        break;
+                    }
+                }
+
+                variableDto.setAffectedComponents(affectedComponentEntities);
+
+                entity.setCanWrite(canWrite);
+                entity.setVariable(inputDto);
+
+                variableEntities.add(entity);
+            }
+        }
+
+        final VariableRegistryDTO registryDto = new VariableRegistryDTO();
+        registryDto.setProcessGroupId(group.getIdentifier());
+        registryDto.setVariables(variableEntities);
+
+        return registryDto;
+    }
+
+
+    /**
+     * Gets the capability description from the specified class.
+     */
+    private String getCapabilityDescription(final Class<?> cls) {
+        final CapabilityDescription capabilityDesc = cls.getAnnotation(CapabilityDescription.class);
+        return capabilityDesc == null ? null : capabilityDesc.value();
+    }
+
+    /**
+     * Gets the tags from the specified class.
+     */
+    private Set<String> getTags(final Class<?> cls) {
+        final Set<String> tags = new HashSet<>();
+        final Tags tagsAnnotation = cls.getAnnotation(Tags.class);
+        if (tagsAnnotation != null) {
+            for (final String tag : tagsAnnotation.value()) {
+                tags.add(tag);
+            }
+        }
+
+        if (cls.isAnnotationPresent(Restricted.class)) {
+            tags.add("restricted");
+        }
+
+        return tags;
+    }
+
+    /**
+     * Creates a bundle DTO from the specified class.
+     *
+     * @param coordinate bundle coordinates
+     * @return dto
+     */
+    public BundleDTO createBundleDto(final BundleCoordinate coordinate) {
+        final BundleDTO dto = new BundleDTO();
+        dto.setGroup(coordinate.getGroup());
+        dto.setArtifact(coordinate.getId());
+        dto.setVersion(coordinate.getVersion());
+        return dto;
+    }
+
+    private List<ControllerServiceApiDTO> createControllerServiceApiDto(final Class cls) {
+        final Set<Class> serviceApis = new HashSet<>();
+
+        // if this is a controller service
+        if (ControllerService.class.isAssignableFrom(cls)) {
+            // get all of it's interfaces to determine the controller service api's it implements
+            final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(cls);
+            for (final Class i : interfaces) {
+                // add all controller services that's not ControllerService itself
+                if (ControllerService.class.isAssignableFrom(i) && !ControllerService.class.equals(i)) {
+                    serviceApis.add(i);
+                }
+            }
+
+            final List<ControllerServiceApiDTO> dtos = new ArrayList<>();
+            for (final Class serviceApi : serviceApis) {
+                final Bundle bundle = extensionManager.getBundle(serviceApi.getClassLoader());
+                final BundleCoordinate bundleCoordinate = bundle.getBundleDetails().getCoordinate();
+
+                final ControllerServiceApiDTO dto = new ControllerServiceApiDTO();
+                dto.setType(serviceApi.getName());
+                dto.setBundle(createBundleDto(bundleCoordinate));
+                dtos.add(dto);
+            }
+            return dtos;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Gets the DocumentedTypeDTOs from the specified classes.
+     *
+     * @param classes classes
+     * @param bundleGroupFilter if specified, must be member of bundle group
+     * @param bundleArtifactFilter if specified, must be member of bundle artifact
+     * @param typeFilter if specified, type must match
+     * @return dtos
+     */
+    public Set<DocumentedTypeDTO> fromDocumentedTypes(final Map<Class, Bundle> classes, final String bundleGroupFilter, final String bundleArtifactFilter, final String typeFilter) {
+        final Set<DocumentedTypeDTO> types = new LinkedHashSet<>();
+        final List<Class> sortedClasses = new ArrayList<>(classes.keySet());
+        Collections.sort(sortedClasses, CLASS_NAME_COMPARATOR);
+
+        for (final Class cls : sortedClasses) {
+            final Bundle bundle = classes.get(cls);
+            final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate();
+
+            // only include classes that meet the criteria if specified
+            if (bundleGroupFilter != null && !bundleGroupFilter.equals(coordinate.getGroup())) {
+                continue;
+            }
+            if (bundleArtifactFilter != null && !bundleArtifactFilter.equals(coordinate.getId())) {
+                continue;
+            }
+            if (typeFilter != null && !typeFilter.equals(cls.getName())) {
+                continue;
+            }
+
+            final DocumentedTypeDTO dto = new DocumentedTypeDTO();
+            dto.setType(cls.getName());
+            dto.setBundle(createBundleDto(coordinate));
+            dto.setControllerServiceApis(createControllerServiceApiDto(cls));
+            dto.setDescription(getCapabilityDescription(cls));
+            dto.setRestricted(isRestricted(cls));
+            dto.setUsageRestriction(getUsageRestriction(cls));
+            dto.setExplicitRestrictions(getExplicitRestrictions(cls));
+            dto.setDeprecationReason(getDeprecationReason(cls));
+            dto.setTags(getTags(cls));
+            types.add(dto);
+        }
+
+        return types;
+    }
+
+    /**
+     * Gets the DocumentedTypeDTOs from the specified classes.
+     *
+     * @param classes classes
+     * @param bundleGroupFilter if specified, must be member of bundle group
+     * @param bundleArtifactFilter if specified, must be member of bundle artifact
+     * @param typeFilter if specified, type must match
+     * @return dtos
+     */
+    public Set<DocumentedTypeDTO> fromDocumentedTypes(final Set<Class> classes, final String bundleGroupFilter, final String bundleArtifactFilter, final String typeFilter) {
+        final Map<Class, Bundle> classBundles = new HashMap<>();
+        for (final Class cls : classes) {
+            classBundles.put(cls, extensionManager.getBundle(cls.getClassLoader()));
+        }
+        return fromDocumentedTypes(classBundles, bundleGroupFilter, bundleArtifactFilter, typeFilter);
+    }
+
+    /**
+     * Creates a ProcessorDTO from the specified ProcessorNode.
+     *
+     * @param node node
+     * @return dto
+     */
+    public ProcessorDTO createProcessorDto(final ProcessorNode node) {
+        if (node == null) {
+            return null;
+        }
+
+        final BundleCoordinate bundleCoordinate = node.getBundleCoordinate();
+        final List<Bundle> compatibleBundles = extensionManager.getBundles(node.getCanonicalClassName()).stream().filter(bundle -> {
+            final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate();
+            return bundleCoordinate.getGroup().equals(coordinate.getGroup()) && bundleCoordinate.getId().equals(coordinate.getId());
+        }).collect(Collectors.toList());
+
+        final ProcessorDTO dto = new ProcessorDTO();
+        dto.setId(node.getIdentifier());
+        dto.setPosition(createPositionDto(node.getPosition()));
+        dto.setStyle(node.getStyle());
+        dto.setParentGroupId(node.getProcessGroup().getIdentifier());
+        dto.setInputRequirement(node.getInputRequirement().name());
+        dto.setPersistsState(node.getProcessor().getClass().isAnnotationPresent(Stateful.class));
+        dto.setRestricted(node.isRestricted());
+        dto.setDeprecated(node.isDeprecated());
+        dto.setExecutionNodeRestricted(node.isExecutionNodeRestricted());
+        dto.setExtensionMissing(node.isExtensionMissing());
+        dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1);
+        dto.setVersionedComponentId(node.getVersionedComponentId().orElse(null));
+
+        dto.setType(node.getCanonicalClassName());
+        dto.setBundle(createBundleDto(bundleCoordinate));
+        dto.setName(node.getName());
+        dto.setState(node.getScheduledState().toString());
+
+        // build the relationship dtos
+        final List<RelationshipDTO> relationships = new ArrayList<>();
+        for (final Relationship rel : node.getRelationships()) {
+            final RelationshipDTO relationshipDTO = new RelationshipDTO();
+            relationshipDTO.setDescription(rel.getDescription());
+            relationshipDTO.setName(rel.getName());
+            relationshipDTO.setAutoTerminate(node.isAutoTerminated(rel));
+            relationships.add(relationshipDTO);
+        }
+
+        // sort the relationships
+        Collections.sort(relationships, new Comparator<RelationshipDTO>() {
+            @Override
+            public int compare(final RelationshipDTO r1, final RelationshipDTO r2) {
+                return Collator.getInstance(Locale.US).compare(r1.getName(), r2.getName());
+            }
+        });
+
+        // set the relationships
+        dto.setRelationships(relationships);
+
+        dto.setDescription(getCapabilityDescription(node.getClass()));
+        dto.setSupportsParallelProcessing(!node.isTriggeredSerially());
+        dto.setSupportsEventDriven(node.isEventDrivenSupported());
+        dto.setSupportsBatching(node.isSessionBatchingSupported());
+        dto.setConfig(createProcessorConfigDto(node));
+
+        final ValidationStatus validationStatus = node.getValidationStatus(1, TimeUnit.MILLISECONDS);
+        dto.setValidationStatus(validationStatus.name());
+
+        final Collection<ValidationResult> validationErrors = node.getValidationErrors();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a BulletinBoardDTO for the specified bulletins.
+     *
+     * @param bulletins bulletins
+     * @return dto
+     */
+    public BulletinBoardDTO createBulletinBoardDto(final List<BulletinEntity> bulletins) {
+        // sort the bulletins
+        Collections.sort(bulletins, new Comparator<BulletinEntity>() {
+            @Override
+            public int compare(final BulletinEntity bulletin1, final BulletinEntity bulletin2) {
+                if (bulletin1 == null && bulletin2 == null) {
+                    return 0;
+                } else if (bulletin1 == null) {
+                    return 1;
+                } else if (bulletin2 == null) {
+                    return -1;
+                }
+
+                final Date timestamp1 = bulletin1.getTimestamp();
+                final Date timestamp2 = bulletin2.getTimestamp();
+                if (timestamp1 == null && timestamp2 == null) {
+                    return 0;
+                } else if (timestamp1 == null) {
+                    return 1;
+                } else if (timestamp2 == null) {
+                    return -1;
+                } else {
+                    return timestamp1.compareTo(timestamp2);
+                }
+            }
+        });
+
+        // create the bulletin board
+        final BulletinBoardDTO bulletinBoard = new BulletinBoardDTO();
+        bulletinBoard.setBulletins(bulletins);
+        bulletinBoard.setGenerated(new Date());
+        return bulletinBoard;
+    }
+
+    /**
+     * Creates BulletinDTOs for the specified Bulletins.
+     *
+     * @param bulletins bulletin
+     * @return dto
+     */
+    public List<BulletinDTO> createBulletinDtos(final List<Bulletin> bulletins) {
+        final List<BulletinDTO> bulletinDtos = new ArrayList<>(bulletins.size());
+        for (final Bulletin bulletin : bulletins) {
+            bulletinDtos.add(createBulletinDto(bulletin));
+        }
+        return bulletinDtos;
+    }
+
+    /**
+     * Creates a BulletinDTO for the specified Bulletin.
+     *
+     * @param bulletin bulletin
+     * @return dto
+     */
+    public BulletinDTO createBulletinDto(final Bulletin bulletin) {
+        final BulletinDTO dto = new BulletinDTO();
+        dto.setId(bulletin.getId());
+        dto.setNodeAddress(bulletin.getNodeAddress());
+        dto.setTimestamp(bulletin.getTimestamp());
+        dto.setGroupId(bulletin.getGroupId());
+        dto.setSourceId(bulletin.getSourceId());
+        dto.setSourceName(bulletin.getSourceName());
+        dto.setCategory(bulletin.getCategory());
+        dto.setLevel(bulletin.getLevel());
+        dto.setMessage(bulletin.getMessage());
+        return dto;
+    }
+
+    /**
+     * Creates a ProvenanceEventNodeDTO for the specified ProvenanceEventLineageNode.
+     *
+     * @param node node
+     * @return dto
+     */
+    public ProvenanceNodeDTO createProvenanceEventNodeDTO(final ProvenanceEventLineageNode node) {
+        final ProvenanceNodeDTO dto = new ProvenanceNodeDTO();
+        dto.setId(node.getIdentifier());
+        dto.setType("EVENT");
+        dto.setEventType(node.getEventType().toString());
+        dto.setTimestamp(new Date(node.getTimestamp()));
+        dto.setMillis(node.getTimestamp());
+        dto.setFlowFileUuid(node.getFlowFileUuid());
+        dto.setParentUuids(node.getParentUuids());
+        dto.setChildUuids(node.getChildUuids());
+        return dto;
+    }
+
+    /**
+     * Creates a FlowFileNodeDTO for the specified LineageNode.
+     *
+     * @param node node
+     * @return dto
+     */
+    public ProvenanceNodeDTO createFlowFileNodeDTO(final LineageNode node) {
+        final ProvenanceNodeDTO dto = new ProvenanceNodeDTO();
+        dto.setId(node.getIdentifier());
+        dto.setType("FLOWFILE");
+        dto.setTimestamp(new Date(node.getTimestamp()));
+        dto.setMillis(node.getTimestamp());
+        dto.setFlowFileUuid(node.getFlowFileUuid());
+        return dto;
+    }
+
+    /**
+     * Creates a ProvenanceLinkDTO for the specified LineageEdge.
+     *
+     * @param edge edge
+     * @return dto
+     */
+    public ProvenanceLinkDTO createProvenanceLinkDTO(final LineageEdge edge) {
+        final LineageNode source = edge.getSource();
+        final LineageNode target = edge.getDestination();
+
+        final ProvenanceLinkDTO dto = new ProvenanceLinkDTO();
+        dto.setTimestamp(new Date(target.getTimestamp()));
+        dto.setMillis(target.getTimestamp());
+        dto.setFlowFileUuid(edge.getUuid());
+        dto.setSourceId(source.getIdentifier());
+        dto.setTargetId(target.getIdentifier());
+        return dto;
+    }
+
+    /**
+     * Creates a LineageDTO for the specified Lineage.
+     *
+     * @param computeLineageSubmission submission
+     * @return dto
+     */
+    public LineageDTO createLineageDto(final ComputeLineageSubmission computeLineageSubmission) {
+        // build the lineage dto
+        final LineageDTO dto = new LineageDTO();
+        final LineageRequestDTO requestDto = new LineageRequestDTO();
+        final LineageResultsDTO resultsDto = new LineageResultsDTO();
+
+        // include the original request and results
+        dto.setRequest(requestDto);
+        dto.setResults(resultsDto);
+
+        // rebuild the request from the submission object
+        switch (computeLineageSubmission.getLineageComputationType()) {
+            case EXPAND_CHILDREN:
+                requestDto.setEventId(computeLineageSubmission.getExpandedEventId());
+                requestDto.setLineageRequestType(LineageRequestType.CHILDREN);
+                break;
+            case EXPAND_PARENTS:
+                requestDto.setEventId(computeLineageSubmission.getExpandedEventId());
+                requestDto.setLineageRequestType(LineageRequestType.PARENTS);
+                break;
+            case FLOWFILE_LINEAGE:
+                final Collection<String> uuids = computeLineageSubmission.getLineageFlowFileUuids();
+                if (uuids.size() == 1) {
+                    requestDto.setUuid(uuids.iterator().next());
+                }
+                requestDto.setEventId(computeLineageSubmission.getExpandedEventId());
+                requestDto.setLineageRequestType(LineageRequestType.FLOWFILE);
+                break;
+        }
+
+        // include lineage details
+        dto.setId(computeLineageSubmission.getLineageIdentifier());
+        dto.setSubmissionTime(computeLineageSubmission.getSubmissionTime());
+
+        // create the results dto
+        final ComputeLineageResult results = computeLineageSubmission.getResult();
+        dto.setFinished(results.isFinished());
+        dto.setPercentCompleted(results.getPercentComplete());
+        dto.setExpiration(results.getExpiration());
+
+        final List<LineageNode> nodes = results.getNodes();
+        final List<LineageEdge> edges = results.getEdges();
+
+        final List<ProvenanceNodeDTO> nodeDtos = new ArrayList<>();
+        if (results.isFinished()) {
+            // create the node dto's
+            for (final LineageNode node : nodes) {
+                switch (node.getNodeType()) {
+                    case FLOWFILE_NODE:
+                        nodeDtos.add(createFlowFileNodeDTO(node));
+                        break;
+                    case PROVENANCE_EVENT_NODE:
+                        nodeDtos.add(createProvenanceEventNodeDTO((ProvenanceEventLineageNode) node));
+                        break;
+                }
+            }
+        }
+        resultsDto.setNodes(nodeDtos);
+
+        // include any errors
+        if (results.getError() != null) {
+            final Set<String> errors = new HashSet<>();
+            errors.add(results.getError());
+            resultsDto.setErrors(errors);
+        }
+
+        // create the link dto's
+        final List<ProvenanceLinkDTO> linkDtos = new ArrayList<>();
+        for (final LineageEdge edge : edges) {
+            linkDtos.add(createProvenanceLinkDTO(edge));
+        }
+        resultsDto.setLinks(linkDtos);
+
+        return dto;
+    }
+
+    /**
+     * Creates a SystemDiagnosticsDTO for the specified SystemDiagnostics.
+     *
+     * @param sysDiagnostics diags
+     * @return dto
+     */
+    public SystemDiagnosticsDTO createSystemDiagnosticsDto(final SystemDiagnostics sysDiagnostics) {
+
+        final SystemDiagnosticsDTO dto = new SystemDiagnosticsDTO();
+        final SystemDiagnosticsSnapshotDTO snapshot = new SystemDiagnosticsSnapshotDTO();
+        dto.setAggregateSnapshot(snapshot);
+
+        snapshot.setStatsLastRefreshed(new Date(sysDiagnostics.getCreationTimestamp()));
+
+        // processors
+        snapshot.setAvailableProcessors(sysDiagnostics.getAvailableProcessors());
+        snapshot.setProcessorLoadAverage(sysDiagnostics.getProcessorLoadAverage());
+
+        // threads
+        snapshot.setDaemonThreads(sysDiagnostics.getDaemonThreads());
+        snapshot.setTotalThreads(sysDiagnostics.getTotalThreads());
+
+        // heap
+        snapshot.setMaxHeap(FormatUtils.formatDataSize(sysDiagnostics.getMaxHeap()));
+        snapshot.setMaxHeapBytes(sysDiagnostics.getMaxHeap());
+        snapshot.setTotalHeap(FormatUtils.formatDataSize(sysDiagnostics.getTotalHeap()));
+        snapshot.setTotalHeapBytes(sysDiagnostics.getTotalHeap());
+        snapshot.setUsedHeap(FormatUtils.formatDataSize(sysDiagnostics.getUsedHeap()));
+        snapshot.setUsedHeapBytes(sysDiagnostics.getUsedHeap());
+        snapshot.setFreeHeap(FormatUtils.formatDataSize(sysDiagnostics.getFreeHeap()));
+        snapshot.setFreeHeapBytes(sysDiagnostics.getFreeHeap());
+        if (sysDiagnostics.getHeapUtilization() != -1) {
+            snapshot.setHeapUtilization(FormatUtils.formatUtilization(sysDiagnostics.getHeapUtilization()));
+        }
+
+        // non heap
+        snapshot.setMaxNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getMaxNonHeap()));
+        snapshot.setMaxNonHeapBytes(sysDiagnostics.getMaxNonHeap());
+        snapshot.setTotalNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getTotalNonHeap()));
+        snapshot.setTotalNonHeapBytes(sysDiagnostics.getTotalNonHeap());
+        snapshot.setUsedNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getUsedNonHeap()));
+        snapshot.setUsedNonHeapBytes(sysDiagnostics.getUsedNonHeap());
+        snapshot.setFreeNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getFreeNonHeap()));
+        snapshot.setFreeNonHeapBytes(sysDiagnostics.getFreeNonHeap());
+        if (sysDiagnostics.getNonHeapUtilization() != -1) {
+            snapshot.setNonHeapUtilization(FormatUtils.formatUtilization(sysDiagnostics.getNonHeapUtilization()));
+        }
+
+        // flow file disk usage
+        final SystemDiagnosticsSnapshotDTO.StorageUsageDTO flowFileRepositoryStorageUsageDto = createStorageUsageDTO(null, sysDiagnostics.getFlowFileRepositoryStorageUsage());
+        snapshot.setFlowFileRepositoryStorageUsage(flowFileRepositoryStorageUsageDto);
+
+        // content disk usage
+        final Set<SystemDiagnosticsSnapshotDTO.StorageUsageDTO> contentRepositoryStorageUsageDtos = new LinkedHashSet<>();
+        snapshot.setContentRepositoryStorageUsage(contentRepositoryStorageUsageDtos);
+        for (final Map.Entry<String, StorageUsage> entry : sysDiagnostics.getContentRepositoryStorageUsage().entrySet()) {
+            contentRepositoryStorageUsageDtos.add(createStorageUsageDTO(entry.getKey(), entry.getValue()));
+        }
+
+        // provenance disk usage
+        final Set<SystemDiagnosticsSnapshotDTO.StorageUsageDTO> provenanceRepositoryStorageUsageDtos = new LinkedHashSet<>();
+        snapshot.setProvenanceRepositoryStorageUsage(provenanceRepositoryStorageUsageDtos);
+        for (final Map.Entry<String, StorageUsage> entry : sysDiagnostics.getProvenanceRepositoryStorageUsage().entrySet()) {
+            provenanceRepositoryStorageUsageDtos.add(createStorageUsageDTO(entry.getKey(), entry.getValue()));
+        }
+
+        // garbage collection
+        final Set<SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO> garbageCollectionDtos = new LinkedHashSet<>();
+        snapshot.setGarbageCollection(garbageCollectionDtos);
+        for (final Map.Entry<String, GarbageCollection> entry : sysDiagnostics.getGarbageCollection().entrySet()) {
+            garbageCollectionDtos.add(createGarbageCollectionDTO(entry.getKey(), entry.getValue()));
+        }
+
+        // version info
+        final SystemDiagnosticsSnapshotDTO.VersionInfoDTO versionInfoDto = createVersionInfoDTO();
+        snapshot.setVersionInfo(versionInfoDto);
+
+        // uptime
+        snapshot.setUptime(FormatUtils.formatHoursMinutesSeconds(sysDiagnostics.getUptime(), TimeUnit.MILLISECONDS));
+
+        return dto;
+    }
+
+    /**
+     * Creates a StorageUsageDTO from the specified StorageUsage.
+     *
+     * @param identifier id
+     * @param storageUsage usage
+     * @return dto
+     */
+    public SystemDiagnosticsSnapshotDTO.StorageUsageDTO createStorageUsageDTO(final String identifier, final StorageUsage storageUsage) {
+        final SystemDiagnosticsSnapshotDTO.StorageUsageDTO dto = new SystemDiagnosticsSnapshotDTO.StorageUsageDTO();
+        dto.setIdentifier(identifier);
+        dto.setFreeSpace(FormatUtils.formatDataSize(storageUsage.getFreeSpace()));
+        dto.setTotalSpace(FormatUtils.formatDataSize(storageUsage.getTotalSpace()));
+        dto.setUsedSpace(FormatUtils.formatDataSize(storageUsage.getUsedSpace()));
+        dto.setFreeSpaceBytes(storageUsage.getFreeSpace());
+        dto.setTotalSpaceBytes(storageUsage.getTotalSpace());
+        dto.setUsedSpaceBytes(storageUsage.getUsedSpace());
+        dto.setUtilization(FormatUtils.formatUtilization(storageUsage.getDiskUtilization()));
+        return dto;
+    }
+
+    /**
+     * Creates a GarbageCollectionDTO from the specified GarbageCollection.
+     *
+     * @param name name
+     * @param garbageCollection gc
+     * @return dto
+     */
+    public SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO createGarbageCollectionDTO(final String name, final GarbageCollection garbageCollection) {
+        final SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO dto = new SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO();
+        dto.setName(name);
+        dto.setCollectionCount(garbageCollection.getCollectionCount());
+        dto.setCollectionTime(FormatUtils.formatHoursMinutesSeconds(garbageCollection.getCollectionTime(), TimeUnit.MILLISECONDS));
+        dto.setCollectionMillis(garbageCollection.getCollectionTime());
+        return dto;
+    }
+
+    public SystemDiagnosticsSnapshotDTO.VersionInfoDTO createVersionInfoDTO() {
+        final SystemDiagnosticsSnapshotDTO.VersionInfoDTO dto = new SystemDiagnosticsSnapshotDTO.VersionInfoDTO();
+        dto.setJavaVendor(System.getProperty("java.vendor"));
+        dto.setJavaVersion(System.getProperty("java.version"));
+        dto.setOsName(System.getProperty("os.name"));
+        dto.setOsVersion(System.getProperty("os.version"));
+        dto.setOsArchitecture(System.getProperty("os.arch"));
+
+        final Bundle frameworkBundle = NarClassLoadersHolder.getInstance().getFrameworkBundle();
+        if (frameworkBundle != null) {
+            final BundleDetails frameworkDetails = frameworkBundle.getBundleDetails();
+
+            dto.setNiFiVersion(frameworkDetails.getCoordinate().getVersion());
+
+            // Get build info
+            dto.setBuildTag(frameworkDetails.getBuildTag());
+            dto.setBuildRevision(frameworkDetails.getBuildRevision());
+            dto.setBuildBranch(frameworkDetails.getBuildBranch());
+            dto.setBuildTimestamp(frameworkDetails.getBuildTimestampDate());
+        }
+
+        return dto;
+    }
+
+    /**
+     * Creates a ResourceDTO from the specified Resource.
+     *
+     * @param resource resource
+     * @return dto
+     */
+    public ResourceDTO createResourceDto(final Resource resource) {
+        final ResourceDTO dto = new ResourceDTO();
+        dto.setIdentifier(resource.getIdentifier());
+        dto.setName(resource.getName());
+        return dto;
+    }
+
+    /**
+     * Creates a ProcessorDiagnosticsDTO from the given Processor and status information with some additional supporting information
+     *
+     * @param procNode the processor to create diagnostics for
+     * @param procStatus the status of given processor
+     * @param bulletinRepo the bulletin repository
+     * @param flowController flowController
+     * @param serviceEntityFactory function for creating a ControllerServiceEntity from a given ID
+     * @return ProcessorDiagnosticsDTO for the given Processor
+     */
+    public ProcessorDiagnosticsDTO createProcessorDiagnosticsDto(final ProcessorNode procNode, final ProcessorStatus procStatus, final BulletinRepository bulletinRepo,
+                                                                 final FlowController flowController, final Function<String, ControllerServiceEntity> serviceEntityFactory) {
+
+        final ProcessorDiagnosticsDTO procDiagnostics = new ProcessorDiagnosticsDTO();
+
+        procDiagnostics.setClassLoaderDiagnostics(createClassLoaderDiagnosticsDto(procNode));
+        procDiagnostics.setIncomingConnections(procNode.getIncomingConnections().stream()
+                .map(this::createConnectionDiagnosticsDto)
+                .collect(Collectors.toSet()));
+        procDiagnostics.setOutgoingConnections(procNode.getConnections().stream()
+                .map(this::createConnectionDiagnosticsDto)
+                .collect(Collectors.toSet()));
+        procDiagnostics.setJvmDiagnostics(createJvmDiagnosticsDto(flowController));
+        procDiagnostics.setProcessor(createProcessorDto(procNode));
+        procDiagnostics.setProcessorStatus(createProcessorStatusDto(procStatus));
+        procDiagnostics.setThreadDumps(createThreadDumpDtos(procNode));
+
+        final Set<ControllerServiceDiagnosticsDTO> referencedServiceDiagnostics = createReferencedServiceDiagnostics(procNode.getProperties(),
+                flowController.getControllerServiceProvider(), serviceEntityFactory);
+        procDiagnostics.setReferencedControllerServices(referencedServiceDiagnostics);
+
+        return procDiagnostics;
+    }
+
+    private Set<ControllerServiceDiagnosticsDTO> createReferencedServiceDiagnostics(final Map<PropertyDescriptor, String> properties, final ControllerServiceProvider serviceProvider,
+                                                                                    final Function<String, ControllerServiceEntity> serviceEntityFactory) {
+
+        final Set<ControllerServiceDiagnosticsDTO> referencedServiceDiagnostics = new HashSet<>();
+        for (final Map.Entry<PropertyDescriptor, String> entry : properties.entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+            if (descriptor.getControllerServiceDefinition() == null) {
+                continue;
+            }
+
+            final String serviceId = entry.getValue();
+            if (serviceId == null) {
+                continue;
+            }
+
+            final ControllerServiceNode serviceNode = serviceProvider.getControllerServiceNode(serviceId);
+            if (serviceNode == null) {
+                continue;
+            }
+
+            final ControllerServiceDiagnosticsDTO serviceDiagnostics = createControllerServiceDiagnosticsDto(serviceNode, serviceEntityFactory, serviceProvider);
+            if (serviceDiagnostics != null) {
+                referencedServiceDiagnostics.add(serviceDiagnostics);
+            }
+        }
+
+        return referencedServiceDiagnostics;
+    }
+
+    /**
+     * Creates a ControllerServiceDiagnosticsDTO from the given Controller Service with some additional supporting information
+     *
+     * @param serviceNode the controller service to create diagnostics for
+     * @param serviceEntityFactory a function to convert a controller service id to a controller service entity
+     * @param serviceProvider the controller service provider
+     * @return ControllerServiceDiagnosticsDTO for the given Controller Service
+     */
+    public ControllerServiceDiagnosticsDTO createControllerServiceDiagnosticsDto(final ControllerServiceNode serviceNode, final Function<String, ControllerServiceEntity> serviceEntityFactory,
+                                                                                 final ControllerServiceProvider serviceProvider) {
+
+        final ControllerServiceDiagnosticsDTO serviceDiagnostics = new ControllerServiceDiagnosticsDTO();
+        final ControllerServiceEntity serviceEntity = serviceEntityFactory.apply(serviceNode.getIdentifier());
+        serviceDiagnostics.setControllerService(serviceEntity);
+
+        serviceDiagnostics.setClassLoaderDiagnostics(createClassLoaderDiagnosticsDto(serviceNode));
+        return serviceDiagnostics;
+    }
+
+
+    private ClassLoaderDiagnosticsDTO createClassLoaderDiagnosticsDto(final ControllerServiceNode serviceNode) {
+        ClassLoader componentClassLoader = extensionManager.getInstanceClassLoader(serviceNode.getIdentifier());
+        if (componentClassLoader == null) {
+            componentClassLoader = serviceNode.getControllerServiceImplementation().getClass().getClassLoader();
+        }
+
+        return createClassLoaderDiagnosticsDto(componentClassLoader);
+    }
+
+
+    private ClassLoaderDiagnosticsDTO createClassLoaderDiagnosticsDto(final ProcessorNode procNode) {
+        ClassLoader componentClassLoader = extensionManager.getInstanceClassLoader(procNode.getIdentifier());
+        if (componentClassLoader == null) {
+            componentClassLoader = procNode.getProcessor().getClass().getClassLoader();
+        }
+
+        return createClassLoaderDiagnosticsDto(componentClassLoader);
+    }
+
+    private ClassLoaderDiagnosticsDTO createClassLoaderDiagnosticsDto(final ClassLoader classLoader) {
+        final ClassLoaderDiagnosticsDTO dto = new ClassLoaderDiagnosticsDTO();
+
+        final Bundle bundle = extensionManager.getBundle(classLoader);
+        if (bundle != null) {
+            dto.setBundle(createBundleDto(bundle.getBundleDetails().getCoordinate()));
+        }
+
+        final ClassLoader parentClassLoader = classLoader.getParent();
+        if (parentClassLoader != null) {
+            dto.setParentClassLoader(createClassLoaderDiagnosticsDto(parentClassLoader));
+        }
+
+        return dto;
+    }
+
+
+    private ConnectionDiagnosticsDTO createConnectionDiagnosticsDto(final Connection connection) {
+        final ConnectionDiagnosticsDTO dto = new ConnectionDiagnosticsDTO();
+        dto.setConnection(createConnectionDto(connection));
+        dto.setAggregateSnapshot(createConnectionDiagnosticsSnapshotDto(connection));
+        return dto;
+    }
+
+    private ConnectionDiagnosticsSnapshotDTO createConnectionDiagnosticsSnapshotDto(final Connection connection) {
+        final ConnectionDiagnosticsSnapshotDTO dto = new ConnectionDiagnosticsSnapshotDTO();
+
+        final QueueDiagnostics queueDiagnostics = connection.getFlowFileQueue().getQueueDiagnostics();
+
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+        final QueueSize totalSize = queue.size();
+        dto.setTotalByteCount(totalSize.getByteCount());
+        dto.setTotalFlowFileCount(totalSize.getObjectCount());
+
+        final LocalQueuePartitionDiagnostics localDiagnostics = queueDiagnostics.getLocalQueuePartitionDiagnostics();
+        dto.setLocalQueuePartition(createLocalQueuePartitionDto(localDiagnostics));
+
+        final List<RemoteQueuePartitionDiagnostics> remoteDiagnostics = queueDiagnostics.getRemoteQueuePartitionDiagnostics();
+        if (remoteDiagnostics != null) {
+            final List<RemoteQueuePartitionDTO> remoteDiagnosticsDtos = remoteDiagnostics.stream()
+                    .map(this::createRemoteQueuePartitionDto)
+                    .collect(Collectors.toList());
+
+            dto.setRemoteQueuePartitions(remoteDiagnosticsDtos);
+        }
+
+        return dto;
+    }
+
+    private LocalQueuePartitionDTO createLocalQueuePartitionDto(final LocalQueuePartitionDiagnostics queueDiagnostics) {
+        final LocalQueuePartitionDTO dto = new LocalQueuePartitionDTO();
+
+        final QueueSize activeSize = queueDiagnostics.getActiveQueueSize();
+        dto.setActiveQueueByteCount(activeSize.getByteCount());
+        dto.setActiveQueueFlowFileCount(activeSize.getObjectCount());
+
+        final QueueSize inFlightSize = queueDiagnostics.getUnacknowledgedQueueSize();
+        dto.setInFlightByteCount(inFlightSize.getByteCount());
+        dto.setInFlightFlowFileCount(inFlightSize.getObjectCount());
+
+        final QueueSize swapSize = queueDiagnostics.getSwapQueueSize();
+        dto.setSwapByteCount(swapSize.getByteCount());
+        dto.setSwapFlowFileCount(swapSize.getObjectCount());
+        dto.setSwapFiles(queueDiagnostics.getSwapFileCount());
+
+        dto.setTotalByteCount(activeSize.getByteCount() + inFlightSize.getByteCount() + swapSize.getByteCount());
+        dto.setTotalFlowFileCount(activeSize.getObjectCount() + inFlightSize.getObjectCount() + swapSize.getObjectCount());
+
+        dto.setAllActiveQueueFlowFilesPenalized(queueDiagnostics.isAllActiveFlowFilesPenalized());
+        dto.setAnyActiveQueueFlowFilesPenalized(queueDiagnostics.isAnyActiveFlowFilePenalized());
+
+        return dto;
+    }
+
+    private RemoteQueuePartitionDTO createRemoteQueuePartitionDto(final RemoteQueuePartitionDiagnostics queueDiagnostics) {
+        final RemoteQueuePartitionDTO dto = new RemoteQueuePartitionDTO();
+
+        dto.setNodeIdentifier(queueDiagnostics.getNodeIdentifier());
+
+        final QueueSize activeSize = queueDiagnostics.getActiveQueueSize();
+        dto.setActiveQueueByteCount(activeSize.getByteCount());
+        dto.setActiveQueueFlowFileCount(activeSize.getObjectCount());
+
+        final QueueSize inFlightSize = queueDiagnostics.getUnacknowledgedQueueSize();
+        dto.setInFlightByteCount(inFlightSize.getByteCount());
+        dto.setInFlightFlowFileCount(inFlightSize.getObjectCount());
+
+        final QueueSize swapSize = queueDiagnostics.getSwapQueueSize();
+        dto.setSwapByteCount(swapSize.getByteCount());
+        dto.setSwapFlowFileCount(swapSize.getObjectCount());
+        dto.setSwapFiles(queueDiagnostics.getSwapFileCount());
+
+        dto.setTotalByteCount(activeSize.getByteCount() + inFlightSize.getByteCount() + swapSize.getByteCount());
+        dto.setTotalFlowFileCount(activeSize.getObjectCount() + inFlightSize.getObjectCount() + swapSize.getObjectCount());
+
+        return dto;
+    }
+
+    private JVMDiagnosticsDTO createJvmDiagnosticsDto(final FlowController flowController) {
+        final JVMDiagnosticsDTO dto = new JVMDiagnosticsDTO();
+        dto.setAggregateSnapshot(createJvmDiagnosticsSnapshotDto(flowController));
+        dto.setClustered(flowController.isClustered());
+        dto.setConnected(flowController.isConnected());
+        return dto;
+    }
+
+    private JVMDiagnosticsSnapshotDTO createJvmDiagnosticsSnapshotDto(final FlowController flowController) {
+        final JVMDiagnosticsSnapshotDTO dto = new JVMDiagnosticsSnapshotDTO();
+
+        final JVMControllerDiagnosticsSnapshotDTO controllerDiagnosticsDto = new JVMControllerDiagnosticsSnapshotDTO();
+        final JVMFlowDiagnosticsSnapshotDTO flowDiagnosticsDto = new JVMFlowDiagnosticsSnapshotDTO();
+        final JVMSystemDiagnosticsSnapshotDTO systemDiagnosticsDto = new JVMSystemDiagnosticsSnapshotDTO();
+
+        dto.setControllerDiagnostics(controllerDiagnosticsDto);
+        dto.setFlowDiagnosticsDto(flowDiagnosticsDto);
+        dto.setSystemDiagnosticsDto(systemDiagnosticsDto);
+
+        final SystemDiagnostics systemDiagnostics = flowController.getSystemDiagnostics();
+
+        // flow-related information
+        final Set<BundleDTO> bundlesLoaded = extensionManager.getAllBundles().stream()
+                .map(bundle -> bundle.getBundleDetails().getCoordinate())
+                .sorted((a, b) -> a.getCoordinate().compareTo(b.getCoordinate()))
+                .map(this::createBundleDto)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+
+        flowDiagnosticsDto.setActiveEventDrivenThreads(flowController.getActiveEventDrivenThreadCount());
+        flowDiagnosticsDto.setActiveTimerDrivenThreads(flowController.getActiveTimerDrivenThreadCount());
+        flowDiagnosticsDto.setBundlesLoaded(bundlesLoaded);
+        flowDiagnosticsDto.setTimeZone(System.getProperty("user.timezone"));
+        flowDiagnosticsDto.setUptime(FormatUtils.formatHoursMinutesSeconds(systemDiagnostics.getUptime(), TimeUnit.MILLISECONDS));
+
+        // controller-related information
+        controllerDiagnosticsDto.setClusterCoordinator(flowController.isClusterCoordinator());
+        controllerDiagnosticsDto.setPrimaryNode(flowController.isPrimary());
+        controllerDiagnosticsDto.setMaxEventDrivenThreads(flowController.getMaxEventDrivenThreadCount());
+        controllerDiagnosticsDto.setMaxTimerDrivenThreads(flowController.getMaxTimerDrivenThreadCount());
+
+        // system-related information
+        systemDiagnosticsDto.setMaxOpenFileDescriptors(systemDiagnostics.getMaxOpenFileHandles());
+        systemDiagnosticsDto.setOpenFileDescriptors(systemDiagnostics.getOpenFileHandles());
+        systemDiagnosticsDto.setPhysicalMemoryBytes(systemDiagnostics.getTotalPhysicalMemory());
+        systemDiagnosticsDto.setPhysicalMemory(FormatUtils.formatDataSize(systemDiagnostics.getTotalPhysicalMemory()));
+
+        final NumberFormat percentageFormat = NumberFormat.getPercentInstance();
+        percentageFormat.setMaximumFractionDigits(2);
+
+        final Set<RepositoryUsageDTO> contentRepoUsage = new HashSet<>();
+        for (final Map.Entry<String, StorageUsage> entry : systemDiagnostics.getContentRepositoryStorageUsage().entrySet()) {
+            final String repoName = entry.getKey();
+            final StorageUsage usage = entry.getValue();
+
+            final RepositoryUsageDTO usageDto = new RepositoryUsageDTO();
+            usageDto.setName(repoName);
+
+            usageDto.setFileStoreHash(DigestUtils.sha256Hex(flowController.getContentRepoFileStoreName(repoName)));
+            usageDto.setFreeSpace(FormatUtils.formatDataSize(usage.getFreeSpace()));
+            usageDto.setFreeSpaceBytes(usage.getFreeSpace());
+            usageDto.setTotalSpace(FormatUtils.formatDataSize(usage.getTotalSpace()));
+            usageDto.setTotalSpaceBytes(usage.getTotalSpace());
+
+            final double usedPercentage = (usage.getTotalSpace() - usage.getFreeSpace()) / (double) usage.getTotalSpace();
+            final String utilization = percentageFormat.format(usedPercentage);
+            usageDto.setUtilization(utilization);
+            contentRepoUsage.add(usageDto);
+        }
+
+        final Set<RepositoryUsageDTO> provRepoUsage = new HashSet<>();
+        for (final Map.Entry<String, StorageUsage> entry : systemDiagnostics.getProvenanceRepositoryStorageUsage().entrySet()) {
+            final String repoName = entry.getKey();
+            final StorageUsage usage = entry.getValue();
+
+            final RepositoryUsageDTO usageDto = new RepositoryUsageDTO();
+            usageDto.setName(repoName);
+
+            usageDto.setFileStoreHash(DigestUtils.sha256Hex(flowController.getProvenanceRepoFileStoreName(repoName)));
+            usageDto.setFreeSpace(FormatUtils.formatDataSize(usage.getFreeSpace()));
+            usageDto.setFreeSpaceBytes(usage.getFreeSpace());
+            usageDto.setTotalSpace(FormatUtils.formatDataSize(usage.getTotalSpace()));
+            usageDto.setTotalSpaceBytes(usage.getTotalSpace());
+
+            final double usedPercentage = (usage.getTotalSpace() - usage.getFreeSpace()) / (double) usage.getTotalSpace();
+            final String utilization = percentageFormat.format(usedPercentage);
+            usageDto.setUtilization(utilization);
+            provRepoUsage.add(usageDto);
+        }
+
+        final RepositoryUsageDTO flowFileRepoUsage = new RepositoryUsageDTO();
+        for (final Map.Entry<String, StorageUsage> entry : systemDiagnostics.getProvenanceRepositoryStorageUsage().entrySet()) {
+            final String repoName = entry.getKey();
+            final StorageUsage usage = entry.getValue();
+
+            flowFileRepoUsage.setName(repoName);
+
+            flowFileRepoUsage.setFileStoreHash(DigestUtils.sha256Hex(flowController.getFlowRepoFileStoreName()));
+            flowFileRepoUsage.setFreeSpace(FormatUtils.formatDataSize(usage.getFreeSpace()));
+            flowFileRepoUsage.setFreeSpaceBytes(usage.getFreeSpace());
+            flowFileRepoUsage.setTotalSpace(FormatUtils.formatDataSize(usage.getTotalSpace()));
+            flowFileRepoUsage.setTotalSpaceBytes(usage.getTotalSpace());
+
+            final double usedPercentage = (usage.getTotalSpace() - usage.getFreeSpace()) / (double) usage.getTotalSpace();
+            final String utilization = percentageFormat.format(usedPercentage);
+            flowFileRepoUsage.setUtilization(utilization);
+        }
+
+        systemDiagnosticsDto.setContentRepositoryStorageUsage(contentRepoUsage);
+        systemDiagnosticsDto.setCpuCores(systemDiagnostics.getAvailableProcessors());
+        systemDiagnosticsDto.setCpuLoadAverage(systemDiagnostics.getProcessorLoadAverage());
+        systemDiagnosticsDto.setFlowFileRepositoryStorageUsage(flowFileRepoUsage);
+        systemDiagnosticsDto.setMaxHeapBytes(systemDiagnostics.getMaxHeap());
+        systemDiagnosticsDto.setMaxHeap(FormatUtils.formatDataSize(systemDiagnostics.getMaxHeap()));
+        systemDiagnosticsDto.setProvenanceRepositoryStorageUsage(provRepoUsage);
+
+        // Create the Garbage Collection History info
+        final GarbageCollectionHistory gcHistory = flowController.getGarbageCollectionHistory();
+        final List<GarbageCollectionDiagnosticsDTO> gcDiagnostics = new ArrayList<>();
+        for (final String memoryManager : gcHistory.getMemoryManagerNames()) {
+            final List<GarbageCollectionStatus> statuses = gcHistory.getGarbageCollectionStatuses(memoryManager);
+
+            final List<GCDiagnosticsSnapshotDTO> gcSnapshots = new ArrayList<>();
+            for (final GarbageCollectionStatus status : statuses) {
+                final GCDiagnosticsSnapshotDTO snapshotDto = new GCDiagnosticsSnapshotDTO();
+                snapshotDto.setTimestamp(status.getTimestamp());
+                snapshotDto.setCollectionCount(status.getCollectionCount());
+                snapshotDto.setCollectionMillis(status.getCollectionMillis());
+                gcSnapshots.add(snapshotDto);
+            }
+
+            gcSnapshots.sort(Comparator.comparing(GCDiagnosticsSnapshotDTO::getTimestamp).reversed());
+
+            final GarbageCollectionDiagnosticsDTO gcDto = new GarbageCollectionDiagnosticsDTO();
+            gcDto.setMemoryManagerName(memoryManager);
+            gcDto.setSnapshots(gcSnapshots);
+            gcDiagnostics.add(gcDto);
+        }
+
+        systemDiagnosticsDto.setGarbageCollectionDiagnostics(gcDiagnostics);
+
+        return dto;
+    }
+
+    private List<ThreadDumpDTO> createThreadDumpDtos(final ProcessorNode procNode) {
+        final List<ThreadDumpDTO> threadDumps = new ArrayList<>();
+
+        final List<ActiveThreadInfo> activeThreads = procNode.getActiveThreads();
+        for (final ActiveThreadInfo threadInfo : activeThreads) {
+            final ThreadDumpDTO dto = new ThreadDumpDTO();
+            dto.setStackTrace(threadInfo.getStackTrace());
+            dto.setThreadActiveMillis(threadInfo.getActiveMillis());
+            dto.setThreadName(threadInfo.getThreadName());
+            dto.setTaskTerminated(threadInfo.isTerminated());
+            threadDumps.add(dto);
+        }
+
+        return threadDumps;
+    }
+
+    /**
+     * Creates a ProcessorConfigDTO from the specified ProcessorNode.
+     *
+     * @param procNode node
+     * @return dto
+     */
+    public ProcessorConfigDTO createProcessorConfigDto(final ProcessorNode procNode) {
+        if (procNode == null) {
+            return null;
+        }
+
+        final ProcessorConfigDTO dto = new ProcessorConfigDTO();
+
+        // sort a copy of the properties
+        final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() {
+            @Override
+            public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) {
+                return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName());
+            }
+        });
+        sortedProperties.putAll(procNode.getProperties());
+
+        // get the property order from the processor
+        final Processor processor = procNode.getProcessor();
+        final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>();
+        final List<PropertyDescriptor> descriptors = processor.getPropertyDescriptors();
+        if (descriptors != null && !descriptors.isEmpty()) {
+            for (final PropertyDescriptor descriptor : descriptors) {
+                orderedProperties.put(descriptor, null);
+            }
+        }
+        orderedProperties.putAll(sortedProperties);
+
+        // build the descriptor and property dtos
+        dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>());
+        dto.setProperties(new LinkedHashMap<String, String>());
+        for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) {
+            final PropertyDescriptor descriptor = entry.getKey();
+
+            // store the property descriptor
+            dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, procNode.getProcessGroup().getIdentifier()));
+
+            // determine the property value - don't include sensitive properties
+            String propertyValue = entry.getValue();
+            if (propertyValue != null && descriptor.isSensitive()) {
+                propertyValue = SENSITIVE_VALUE_MASK;
+            } else if (propertyValue == null && descriptor.getDefaultValue() != null) {
+                propertyValue = descriptor.getDefaultValue();
+            }
+
+            // set the property value
+            dto.getProperties().put(descriptor.getName(), propertyValue);
+        }
+
+        dto.setSchedulingPeriod(procNode.getSchedulingPeriod());
+        dto.setPenaltyDuration(procNode.getPenalizationPeriod());
+        dto.setYieldDuration(procNode.getYieldPeriod());
+        dto.setRunDurationMillis(procNode.getRunDuration(TimeUnit.MILLISECONDS));
+        dto.setConcurrentlySchedulableTaskCount(procNode.getMaxConcurrentTasks());
+        dto.setLossTolerant(procNode.isLossTolerant());
+        dto.setComments(procNode.getComments());
+        dto.setBulletinLevel(procNode.getBulletinLevel().name());
+        dto.setSchedulingStrategy(procNode.getSchedulingStrategy().name());
+        dto.setExecutionNode(procNode.getExecutionNode().name());
+        dto.setAnnotationData(procNode.getAnnotationData());
+
+        // set up the default values for concurrent tasks and scheduling period
+        final Map<String, String> defaultConcurrentTasks = new HashMap<>();
+        defaultConcurrentTasks.put(SchedulingStrategy.TIMER_DRIVEN.name(), String.valueOf(SchedulingStrategy.TIMER_DRIVEN.getDefaultConcurrentTasks()));
+        defaultConcurrentTasks.put(SchedulingStrategy.EVENT_DRIVEN.name(), String.valueOf(SchedulingStrategy.EVENT_DRIVEN.getDefaultConcurrentTasks()));
+        defaultConcurrentTasks.put(SchedulingStrategy.CRON_DRIVEN.name(), String.valueOf(SchedulingStrategy.CRON_DRIVEN.getDefaultConcurrentTasks()));
+        dto.setDefaultConcurrentTasks(defaultConcurrentTasks);
+
+        final Map<String, String> defaultSchedulingPeriod = new HashMap<>();
+        defaultSchedulingPeriod.put(SchedulingStrategy.TIMER_DRIVEN.name(), SchedulingStrategy.TIMER_DRIVEN.getDefaultSchedulingPeriod());
+        defaultSchedulingPeriod.put(SchedulingStrategy.CRON_DRIVEN.name(), SchedulingStrategy.CRON_DRIVEN.getDefaultSchedulingPeriod());
+        dto.setDefaultSchedulingPeriod(defaultSchedulingPeriod);
+
+        return dto;
+    }
+
+    /**
+     * Creates a PropertyDesriptorDTO from the specified PropertyDesriptor.
+     *
+     * @param propertyDescriptor descriptor
+     * @param groupId the Identifier of the Process Group that the component belongs to
+     * @return dto
+     */
+    public PropertyDescriptorDTO createPropertyDescriptorDto(final PropertyDescriptor propertyDescriptor, final String groupId) {
+        if (propertyDescriptor == null) {
+            return null;
+        }
+
+        final PropertyDescriptorDTO dto = new PropertyDescriptorDTO();
+
+        dto.setName(propertyDescriptor.getName());
+        dto.setDisplayName(propertyDescriptor.getDisplayName());
+        dto.setRequired(propertyDescriptor.isRequired());
+        dto.setSensitive(propertyDescriptor.isSensitive());
+        dto.setDynamic(propertyDescriptor.isDynamic());
+        dto.setDescription(propertyDescriptor.getDescription());
+        dto.setDefaultValue(propertyDescriptor.getDefaultValue());
+        dto.setSupportsEl(propertyDescriptor.isExpressionLanguageSupported());
+
+        // to support legacy/deprecated method .expressionLanguageSupported(true)
+        String description = propertyDescriptor.isExpressionLanguageSupported()
+                && propertyDescriptor.getExpressionLanguageScope().equals(ExpressionLanguageScope.NONE)
+                ? "true (undefined scope)" : propertyDescriptor.getExpressionLanguageScope().getDescription();
+        dto.setExpressionLanguageScope(description);
+
+        // set the identifies controller service is applicable
+        if (propertyDescriptor.getControllerServiceDefinition() != null) {
+            final Class serviceClass = propertyDescriptor.getControllerServiceDefinition();
+            final Bundle serviceBundle = extensionManager.getBundle(serviceClass.getClassLoader());
+
+            dto.setIdentifiesControllerService(serviceClass.getName());
+            dto.setIdentifiesControllerServiceBundle(createBundleDto(serviceBundle.getBundleDetails().getCoordinate()));
+        }
+
+        final Class<? extends ControllerService> serviceDefinition = propertyDescriptor.getControllerServiceDefinition();
+        if (propertyDescriptor.getAllowableValues() == null) {
+            if (serviceDefinition == null) {
+                dto.setAllowableValues(null);
+            } else {
+                final List<AllowableValueEntity> allowableValues = new ArrayList<>();
+                final List<String> controllerServiceIdentifiers = new ArrayList<>(controllerServiceProvider.getControllerServiceIdentifiers(serviceDefinition, groupId));
+                Collections.sort(controllerServiceIdentifiers, Collator.getInstance(Locale.US));
+                for (final String serviceIdentifier : controllerServiceIdentifiers) {
+                    final ControllerServiceNode service = controllerServiceProvider.getControllerServiceNode(serviceIdentifier);
+                    final boolean isServiceAuthorized = service.isAuthorized(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+                    final String displayName = isServiceAuthorized ? service.getName() : serviceIdentifier;
+
+                    final AllowableValueDTO allowableValue = new AllowableValueDTO();
+                    allowableValue.setDisplayName(displayName);
+                    allowableValue.setValue(serviceIdentifier);
+                    allowableValues.add(entityFactory.createAllowableValueEntity(allowableValue, isServiceAuthorized));
+                }
+                dto.setAllowableValues(allowableValues);
+            }
+        } else {
+            final List<AllowableValueEntity> allowableValues = new ArrayList<>();
+            for (final AllowableValue allowableValue : propertyDescriptor.getAllowableValues()) {
+                final AllowableValueDTO allowableValueDto = new AllowableValueDTO();
+                allowableValueDto.setDisplayName(allowableValue.getDisplayName());
+                allowableValueDto.setValue(allowableValue.getValue());
+                allowableValueDto.setDescription(allowableValue.getDescription());
+                allowableValues.add(entityFactory.createAllowableValueEntity(allowableValueDto, true));
+            }
+
+            dto.setAllowableValues(allowableValues);
+        }
+
+        return dto;
+    }
+
+    // Copy methods
+    public LabelDTO copy(final LabelDTO original) {
+        final LabelDTO copy = new LabelDTO();
+        copy.setId(original.getId());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setLabel(original.getLabel());
+        copy.setStyle(copy(original.getStyle()));
+        copy.setPosition(original.getPosition());
+        copy.setWidth(original.getWidth());
+        copy.setHeight(original.getHeight());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+
+        return copy;
+    }
+
+    public ControllerServiceDTO copy(final ControllerServiceDTO original) {
+        final ControllerServiceDTO copy = new ControllerServiceDTO();
+        copy.setAnnotationData(original.getAnnotationData());
+        copy.setControllerServiceApis(original.getControllerServiceApis());
+        copy.setComments(original.getComments());
+        copy.setCustomUiUrl(original.getCustomUiUrl());
+        copy.setDescriptors(copy(original.getDescriptors()));
+        copy.setId(original.getId());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setName(original.getName());
+        copy.setProperties(copy(original.getProperties()));
+        copy.setReferencingComponents(copy(original.getReferencingComponents()));
+        copy.setState(original.getState());
+        copy.setType(original.getType());
+        copy.setBundle(copy(original.getBundle()));
+        copy.setExtensionMissing(original.getExtensionMissing());
+        copy.setMultipleVersionsAvailable(original.getMultipleVersionsAvailable());
+        copy.setPersistsState(original.getPersistsState());
+        copy.setValidationErrors(copy(original.getValidationErrors()));
+        copy.setValidationStatus(original.getValidationStatus());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+        return copy;
+    }
+
+    public FunnelDTO copy(final FunnelDTO original) {
+        final FunnelDTO copy = new FunnelDTO();
+        copy.setId(original.getId());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setPosition(original.getPosition());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+
+        return copy;
+    }
+
+    private <T> List<T> copy(final List<T> original) {
+        if (original == null) {
+            return null;
+        } else {
+            return new ArrayList<>(original);
+        }
+    }
+
+    private <T> List<T> copy(final Collection<T> original) {
+        if (original == null) {
+            return null;
+        } else {
+            return new ArrayList<>(original);
+        }
+    }
+
+    private <T> Set<T> copy(final Set<T> original) {
+        if (original == null) {
+            return null;
+        } else {
+            return new LinkedHashSet<>(original);
+        }
+    }
+
+    private <S, T> Map<S, T> copy(final Map<S, T> original) {
+        if (original == null) {
+            return null;
+        } else {
+            return new LinkedHashMap<>(original);
+        }
+    }
+
+    public BundleDTO copy(final BundleDTO original) {
+        if (original == null) {
+            return null;
+        }
+
+        final BundleDTO copy = new BundleDTO();
+        copy.setGroup(original.getGroup());
+        copy.setArtifact(original.getArtifact());
+        copy.setVersion(original.getVersion());
+        return copy;
+    }
+
+    public ProcessorDTO copy(final ProcessorDTO original) {
+        final ProcessorDTO copy = new ProcessorDTO();
+        copy.setConfig(copy(original.getConfig()));
+        copy.setPosition(original.getPosition());
+        copy.setId(original.getId());
+        copy.setName(original.getName());
+        copy.setDescription(original.getDescription());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setRelationships(copy(original.getRelationships()));
+        copy.setState(original.getState());
+        copy.setStyle(copy(original.getStyle()));
+        copy.setType(original.getType());
+        copy.setBundle(copy(original.getBundle()));
+        copy.setSupportsParallelProcessing(original.getSupportsParallelProcessing());
+        copy.setSupportsEventDriven(original.getSupportsEventDriven());
+        copy.setSupportsBatching(original.getSupportsBatching());
+        copy.setPersistsState(original.getPersistsState());
+        copy.setExecutionNodeRestricted(original.isExecutionNodeRestricted());
+        copy.setExtensionMissing(original.getExtensionMissing());
+        copy.setMultipleVersionsAvailable(original.getMultipleVersionsAvailable());
+        copy.setValidationErrors(copy(original.getValidationErrors()));
+        copy.setValidationStatus(original.getValidationStatus());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+
+        return copy;
+    }
+
+    private ProcessorConfigDTO copy(final ProcessorConfigDTO original) {
+        final ProcessorConfigDTO copy = new ProcessorConfigDTO();
+        copy.setAnnotationData(original.getAnnotationData());
+        copy.setAutoTerminatedRelationships(copy(original.getAutoTerminatedRelationships()));
+        copy.setComments(original.getComments());
+        copy.setSchedulingStrategy(original.getSchedulingStrategy());
+        copy.setExecutionNode(original.getExecutionNode());
+        copy.setConcurrentlySchedulableTaskCount(original.getConcurrentlySchedulableTaskCount());
+        copy.setCustomUiUrl(original.getCustomUiUrl());
+        copy.setDescriptors(copy(original.getDescriptors()));
+        copy.setProperties(copy(original.getProperties()));
+        copy.setSchedulingPeriod(original.getSchedulingPeriod());
+        copy.setPenaltyDuration(original.getPenaltyDuration());
+        copy.setYieldDuration(original.getYieldDuration());
+        copy.setRunDurationMillis(original.getRunDurationMillis());
+        copy.setBulletinLevel(original.getBulletinLevel());
+        copy.setDefaultConcurrentTasks(original.getDefaultConcurrentTasks());
+        copy.setDefaultSchedulingPeriod(original.getDefaultSchedulingPeriod());
+        copy.setLossTolerant(original.isLossTolerant());
+
+        return copy;
+    }
+
+    public ConnectionDTO copy(final ConnectionDTO original) {
+        final ConnectionDTO copy = new ConnectionDTO();
+        copy.setAvailableRelationships(copy(original.getAvailableRelationships()));
+        copy.setDestination(original.getDestination());
+        copy.setPosition(original.getPosition());
+        copy.setId(original.getId());
+        copy.setName(original.getName());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setSelectedRelationships(copy(original.getSelectedRelationships()));
+        copy.setFlowFileExpiration(original.getFlowFileExpiration());
+        copy.setBackPressureObjectThreshold(original.getBackPressureObjectThreshold());
+        copy.setBackPressureDataSizeThreshold(original.getBackPressureDataSizeThreshold());
+        copy.setPrioritizers(copy(original.getPrioritizers()));
+        copy.setSource(original.getSource());
+        copy.setzIndex(original.getzIndex());
+        copy.setLabelIndex(original.getLabelIndex());
+        copy.setBends(copy(original.getBends()));
+        copy.setLoadBalancePartitionAttribute(original.getLoadBalancePartitionAttribute());
+        copy.setLoadBalanceStrategy(original.getLoadBalanceStrategy());
+        copy.setLoadBalanceCompression(original.getLoadBalanceCompression());
+        copy.setLoadBalanceStatus(original.getLoadBalanceStatus());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+
+        return copy;
+    }
+
+    public BulletinDTO copy(final BulletinDTO original) {
+        final BulletinDTO copy = new BulletinDTO();
+        copy.setId(original.getId());
+        copy.setTimestamp(original.getTimestamp());
+        copy.setGroupId(original.getGroupId());
+        copy.setSourceId(original.getSourceId());
+        copy.setSourceName(original.getSourceName());
+        copy.setCategory(original.getCategory());
+        copy.setLevel(original.getLevel());
+        copy.setMessage(original.getMessage());
+        copy.setNodeAddress(original.getNodeAddress());
+        return copy;
+    }
+
+    public PortDTO copy(final PortDTO original) {
+        final PortDTO copy = new PortDTO();
+        copy.setPosition(original.getPosition());
+        copy.setId(original.getId());
+        copy.setName(original.getName());
+        copy.setComments(original.getComments());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setState(original.getState());
+        copy.setType(original.getType());
+        copy.setTransmitting(original.isTransmitting());
+        copy.setConcurrentlySchedulableTaskCount(original.getConcurrentlySchedulableTaskCount());
+        copy.setUserAccessControl(copy(original.getUserAccessControl()));
+        copy.setGroupAccessControl(copy(original.getGroupAccessControl()));
+        copy.setValidationErrors(copy(original.getValidationErrors()));
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+        return copy;
+    }
+
+    public RemoteProcessGroupPortDTO copy(final RemoteProcessGroupPortDTO original) {
+        final RemoteProcessGroupPortDTO copy = new RemoteProcessGroupPortDTO();
+        copy.setId(original.getId());
+        copy.setTargetId(original.getTargetId());
+        copy.setGroupId(original.getGroupId());
+        copy.setName(original.getName());
+        copy.setComments(original.getComments());
+        copy.setConnected(original.isConnected());
+        copy.setTargetRunning(original.isTargetRunning());
+        copy.setTransmitting(original.isTransmitting());
+        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();
+            batchCopy.setCount(batchOrg.getCount());
+            batchCopy.setSize(batchOrg.getSize());
+            batchCopy.setDuration(batchOrg.getDuration());
+            copy.setBatchSettings(batchCopy);
+        }
+        return copy;
+    }
+
+    public ProcessGroupDTO copy(final ProcessGroupDTO original, final boolean deep) {
+        final ProcessGroupDTO copy = new ProcessGroupDTO();
+        copy.setComments(original.getComments());
+        copy.setContents(copy(original.getContents(), deep));
+        copy.setPosition(original.getPosition());
+        copy.setId(original.getId());
+        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());
+        copy.setDisabledCount(original.getDisabledCount());
+        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()));
+        }
+
+        return copy;
+    }
+
+    public VersionControlInformationDTO copy(final VersionControlInformationDTO original) {
+        if (original == null) {
+            return null;
+        }
+
+        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.setState(original.getState());
+        copy.setStateExplanation(original.getStateExplanation());
+        return copy;
+    }
+
+    public RemoteProcessGroupDTO copy(final RemoteProcessGroupDTO original) {
+        final RemoteProcessGroupContentsDTO originalContents = original.getContents();
+        final RemoteProcessGroupContentsDTO copyContents = new RemoteProcessGroupContentsDTO();
+
+        if (originalContents.getInputPorts() != null) {
+            final Set<RemoteProcessGroupPortDTO> inputPorts = new HashSet<>();
+            for (final RemoteProcessGroupPortDTO port : originalContents.getInputPorts()) {
+                inputPorts.add(copy(port));
+            }
+            copyContents.setInputPorts(inputPorts);
+        }
+
+        if (originalContents.getOutputPorts() != null) {
+            final Set<RemoteProcessGroupPortDTO> outputPorts = new HashSet<>();
+            for (final RemoteProcessGroupPortDTO port : originalContents.getOutputPorts()) {
+                outputPorts.add(copy(port));
+            }
+            copyContents.setOutputPorts(outputPorts);
+        }
+
+        final RemoteProcessGroupDTO copy = new RemoteProcessGroupDTO();
+        copy.setComments(original.getComments());
+        copy.setPosition(original.getPosition());
+        copy.setId(original.getId());
+        copy.setCommunicationsTimeout(original.getCommunicationsTimeout());
+        copy.setYieldDuration(original.getYieldDuration());
+        copy.setName(original.getName());
+        copy.setInputPortCount(original.getInputPortCount());
+        copy.setOutputPortCount(original.getOutputPortCount());
+        copy.setActiveRemoteInputPortCount(original.getActiveRemoteInputPortCount());
+        copy.setInactiveRemoteInputPortCount(original.getInactiveRemoteInputPortCount());
+        copy.setActiveRemoteOutputPortCount(original.getActiveRemoteOutputPortCount());
+        copy.setInactiveRemoteOutputPortCount(original.getInactiveRemoteOutputPortCount());
+        copy.setParentGroupId(original.getParentGroupId());
+        copy.setTargetUris(original.getTargetUris());
+        copy.setTransportProtocol(original.getTransportProtocol());
+        copy.setProxyHost(original.getProxyHost());
+        copy.setProxyPort(original.getProxyPort());
+        copy.setProxyUser(original.getProxyUser());
+        copy.setProxyPassword(original.getProxyPassword());
+        copy.setLocalNetworkInterface(original.getLocalNetworkInterface());
+        copy.setVersionedComponentId(original.getVersionedComponentId());
+
+        copy.setContents(copyContents);
+
+        return copy;
+    }
+
+    public ConnectableDTO createConnectableDto(final PortDTO port, final ConnectableType type) {
+        final ConnectableDTO connectable = new ConnectableDTO();
+        connectable.setGroupId(port.getParentGroupId());
+        connectable.setId(port.getId());
+        connectable.setName(port.getName());
+        connectable.setType(type.name());
+        connectable.setVersionedComponentId(port.getVersionedComponentId());
+        return connectable;
+    }
+
+    public ConnectableDTO createConnectableDto(final ProcessorDTO processor) {
+        final ConnectableDTO connectable = new ConnectableDTO();
+        connectable.setGroupId(processor.getParentGroupId());
+        connectable.setId(processor.getId());
+        connectable.setName(processor.getName());
+        connectable.setType(ConnectableType.PROCESSOR.name());
+        connectable.setVersionedComponentId(processor.getVersionedComponentId());
+        return connectable;
+    }
+
+    public ConnectableDTO createConnectableDto(final FunnelDTO funnel) {
+        final ConnectableDTO connectable = new ConnectableDTO();
+        connectable.setGroupId(funnel.getParentGroupId());
+        connectable.setId(funnel.getId());
+        connectable.setType(ConnectableType.FUNNEL.name());
+        connectable.setVersionedComponentId(funnel.getVersionedComponentId());
+        return connectable;
+    }
+
+    public ConnectableDTO createConnectableDto(final RemoteProcessGroupPortDTO remoteGroupPort, final ConnectableType type) {
+        final ConnectableDTO connectable = new ConnectableDTO();
+        connectable.setGroupId(remoteGroupPort.getGroupId());
+        connectable.setId(remoteGroupPort.getId());
+        connectable.setName(remoteGroupPort.getName());
+        connectable.setType(type.name());
+        connectable.setVersionedComponentId(connectable.getVersionedComponentId());
+        return connectable;
+    }
+
+    /**
+     *
+     * @param original orig
+     * @param deep if <code>true</code>, all Connections, ProcessGroups, Ports, Processors, etc. will be copied. If <code>false</code>, the copy will have links to the same objects referenced by
+     * <code>original</code>.
+     *
+     * @return dto
+     */
+    private FlowSnippetDTO copy(final FlowSnippetDTO original, final boolean deep) {
+        final FlowSnippetDTO copy = new FlowSnippetDTO();
+
+        final Set<ConnectionDTO> connections = new LinkedHashSet<>();
+        final Set<ProcessGroupDTO> groups = new LinkedHashSet<>();
+        final Set<PortDTO> inputPorts = new LinkedHashSet<>();
+        final Set<PortDTO> outputPorts = new LinkedHashSet<>();
+        final Set<LabelDTO> labels = new LinkedHashSet<>();
+        final Set<ProcessorDTO> processors = new LinkedHashSet<>();
+        final Set<RemoteProcessGroupDTO> remoteProcessGroups = new LinkedHashSet<>();
+        final Set<FunnelDTO> funnels = new LinkedHashSet<>();
+        final Set<ControllerServiceDTO> controllerServices = new LinkedHashSet<>();
+
+        if (deep) {
+            for (final ProcessGroupDTO group : original.getProcessGroups()) {
+                groups.add(copy(group, deep));
+            }
+
+            for (final PortDTO port : original.getInputPorts()) {
+                inputPorts.add(copy(port));
+            }
+
+            for (final PortDTO port : original.getOutputPorts()) {
+                outputPorts.add(copy(port));
+            }
+
+            for (final LabelDTO label : original.getLabels()) {
+                labels.add(copy(label));
+            }
+
+            for (final ProcessorDTO processor : original.getProcessors()) {
+                processors.add(copy(processor));
+            }
+
+            for (final RemoteProcessGroupDTO remoteGroup : original.getRemoteProcessGroups()) {
+                remoteProcessGroups.add(copy(remoteGroup));
+            }
+
+            for (final FunnelDTO funnel : original.getFunnels()) {
+                funnels.add(copy(funnel));
+            }
+
+            for (final ConnectionDTO connection : original.getConnections()) {
+                connections.add(copy(connection));
+            }
+
+            for (final ControllerServiceDTO controllerService : original.getControllerServices()) {
+                controllerServices.add(copy(controllerService));
+            }
+        } else {
+            if (original.getConnections() != null) {
+                connections.addAll(copy(original.getConnections()));
+            }
+            if (original.getProcessGroups() != null) {
+                groups.addAll(copy(original.getProcessGroups()));
+            }
+            if (original.getInputPorts() != null) {
+                inputPorts.addAll(copy(original.getInputPorts()));
+            }
+            if (original.getOutputPorts() != null) {
+                outputPorts.addAll(copy(original.getOutputPorts()));
+            }
+            if (original.getLabels() != null) {
+                labels.addAll(copy(original.getLabels()));
+            }
+            if (original.getProcessors() != null) {
+                processors.addAll(copy(original.getProcessors()));
+            }
+            if (original.getRemoteProcessGroups() != null) {
+                remoteProcessGroups.addAll(copy(original.getRemoteProcessGroups()));
+            }
+            if (original.getFunnels() != null) {
+                funnels.addAll(copy(original.getFunnels()));
+            }
+            if (original.getControllerServices() != null) {
+                controllerServices.addAll(copy(original.getControllerServices()));
+            }
+        }
+
+        copy.setConnections(connections);
+        copy.setProcessGroups(groups);
+        copy.setInputPorts(inputPorts);
+        copy.setLabels(labels);
+        copy.setOutputPorts(outputPorts);
+        copy.setProcessors(processors);
+        copy.setRemoteProcessGroups(remoteProcessGroups);
+        copy.setFunnels(funnels);
+        copy.setControllerServices(controllerServices);
+
+        return copy;
+    }
+
+    /**
+     * Factory method for creating a new RevisionDTO based on this controller.
+     *
+     * @param lastMod mod
+     * @return dto
+     */
+    public RevisionDTO createRevisionDTO(final FlowModification lastMod) {
+        final Revision revision = lastMod.getRevision();
+
+        // create the dto
+        final RevisionDTO revisionDTO = new RevisionDTO();
+        revisionDTO.setVersion(revision.getVersion());
+        revisionDTO.setClientId(revision.getClientId());
+        revisionDTO.setLastModifier(lastMod.getLastModifier());
+
+        return revisionDTO;
+    }
+
+    public RevisionDTO createRevisionDTO(final Revision revision) {
+        final RevisionDTO dto = new RevisionDTO();
+        dto.setVersion(revision.getVersion());
+        dto.setClientId(revision.getClientId());
+        return dto;
+    }
+
+    public NodeDTO createNodeDTO(final NodeIdentifier nodeId, final NodeConnectionStatus status, final NodeHeartbeat nodeHeartbeat, final List<NodeEvent> events, final Set<String> roles) {
+        final NodeDTO nodeDto = new NodeDTO();
+
+        // populate node dto
+        nodeDto.setNodeId(nodeId.getId());
+        nodeDto.setAddress(nodeId.getApiAddress());
+        nodeDto.setApiPort(nodeId.getApiPort());
+        nodeDto.setStatus(status.getState().name());
+        nodeDto.setRoles(roles);
+        if (status.getConnectionRequestTime() != null) {
+            final Date connectionRequested = new Date(status.getConnectionRequestTime());
+            nodeDto.setConnectionRequested(connectionRequested);
+        }
+
+        // only connected nodes have heartbeats
+        if (nodeHeartbeat != null) {
+            final Date heartbeat = new Date(nodeHeartbeat.getTimestamp());
+            nodeDto.setHeartbeat(heartbeat);
+            nodeDto.setNodeStartTime(new Date(nodeHeartbeat.getSystemStartTime()));
+            nodeDto.setActiveThreadCount(nodeHeartbeat.getActiveThreadCount());
+            nodeDto.setQueued(FormatUtils.formatCount(nodeHeartbeat.getFlowFileCount()) + " / " + FormatUtils.formatDataSize(nodeHeartbeat.getFlowFileBytes()));
+        }
+
+        // populate node events
+        final List<NodeEvent> nodeEvents = new ArrayList<>(events);
+        Collections.sort(nodeEvents, new Comparator<NodeEvent>() {
+            @Override
+            public int compare(final NodeEvent event1, final NodeEvent event2) {
+                return new Date(event2.getTimestamp()).compareTo(new Date(event1.getTimestamp()));
+            }
+        });
+
+        // create the node event dtos
+        final List<NodeEventDTO> nodeEventDtos = new ArrayList<>();
+        for (final NodeEvent event : nodeEvents) {
+            // create node event dto
+            final NodeEventDTO nodeEventDto = new NodeEventDTO();
+            nodeEventDtos.add(nodeEventDto);
+
+            // populate node event dto
+            nodeEventDto.setMessage(event.getMessage());
+            nodeEventDto.setCategory(event.getSeverity().name());
+            nodeEventDto.setTimestamp(new Date(event.getTimestamp()));
+        }
+        nodeDto.setEvents(nodeEventDtos);
+
+        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) {
+        this.controllerServiceProvider = controllerServiceProvider;
+    }
+
+    public void setAuthorizer(final Authorizer authorizer) {
+        this.authorizer = authorizer;
+    }
+
+    public void setEntityFactory(final EntityFactory entityFactory) {
+        this.entityFactory = entityFactory;
+    }
+
+    public void setBulletinRepository(BulletinRepository bulletinRepository) {
+        this.bulletinRepository = bulletinRepository;
+    }
+
+    public void setExtensionManager(ExtensionManager extensionManager) {
+        this.extensionManager = extensionManager;
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java
new file mode 100644 (file)
index 0000000..2dd91ad
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.web.api.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.util.TimeAdapter;
+
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.util.Date;
+
+/**
+ * Details for the controller configuration.
+ */
+@XmlType(name = "flowConfiguration")
+public class FlowConfigurationDTO {
+
+    private Boolean supportsManagedAuthorizer;
+    private Boolean supportsConfigurableAuthorizer;
+    private Boolean supportsConfigurableUsersAndGroups;
+    private Long autoRefreshIntervalSeconds;
+
+    private Date currentTime;
+    private Integer timeOffset;
+
+    private Long defaultBackPressureObjectThreshold;
+    private String defaultBackPressureDataSizeThreshold;
+
+    private String dcaeDistributorApiHostname;
+
+    /**
+     * @author Renu
+     * @return getter and setter for dcae distributor api hostname. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether it picks up configurable host address.",
+            readOnly = true
+    )
+    public String getDcaeDistributorApiHostname() {
+        return dcaeDistributorApiHostname;
+    }
+
+    public void setDcaeDistributorApiHostname(String dcaeDistributorApiHostname) {
+        this.dcaeDistributorApiHostname = dcaeDistributorApiHostname;
+    }
+
+    /**
+     * @return interval in seconds between the automatic NiFi refresh requests. This value is read only
+     */
+    @ApiModelProperty(
+            value = "The interval in seconds between the automatic NiFi refresh requests.",
+            readOnly = true
+    )
+    public Long getAutoRefreshIntervalSeconds() {
+        return autoRefreshIntervalSeconds;
+    }
+
+    public void setAutoRefreshIntervalSeconds(Long autoRefreshIntervalSeconds) {
+        this.autoRefreshIntervalSeconds = autoRefreshIntervalSeconds;
+    }
+
+    /**
+     * @return whether this NiFi supports a managed authorizer. Managed authorizers can visualize users, groups,
+     * and policies in the UI. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi supports a managed authorizer. Managed authorizers can visualize users, groups, and policies in the UI.",
+            readOnly = true
+    )
+    public Boolean getSupportsManagedAuthorizer() {
+        return supportsManagedAuthorizer;
+    }
+
+    public void setSupportsManagedAuthorizer(Boolean supportsManagedAuthorizer) {
+        this.supportsManagedAuthorizer = supportsManagedAuthorizer;
+    }
+
+    /**
+     * @return whether this NiFi supports configurable users and groups. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi supports configurable users and groups.",
+            readOnly = true
+    )
+    public Boolean getSupportsConfigurableUsersAndGroups() {
+        return supportsConfigurableUsersAndGroups;
+    }
+
+    public void setSupportsConfigurableUsersAndGroups(Boolean supportsConfigurableUsersAndGroups) {
+        this.supportsConfigurableUsersAndGroups = supportsConfigurableUsersAndGroups;
+    }
+
+    /**
+     * @return whether this NiFi supports a configurable authorizer. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi supports a configurable authorizer.",
+            readOnly = true
+    )
+    public Boolean getSupportsConfigurableAuthorizer() {
+        return supportsConfigurableAuthorizer;
+    }
+
+    public void setSupportsConfigurableAuthorizer(Boolean supportsConfigurableAuthorizer) {
+        this.supportsConfigurableAuthorizer = supportsConfigurableAuthorizer;
+    }
+
+    /**
+     * @return current time on the server
+     */
+    @XmlJavaTypeAdapter(TimeAdapter.class)
+    @ApiModelProperty(
+            value = "The current time on the system.",
+            dataType = "string"
+    )
+    public Date getCurrentTime() {
+        return currentTime;
+    }
+
+    public void setCurrentTime(Date currentTime) {
+        this.currentTime = currentTime;
+    }
+
+    /**
+     * @return time offset of the server
+     */
+    @ApiModelProperty(
+            value = "The time offset of the system."
+    )
+    public Integer getTimeOffset() {
+        return timeOffset;
+    }
+
+    public void setTimeOffset(Integer timeOffset) {
+        this.timeOffset = timeOffset;
+    }
+
+    /**
+     * @return the default back pressure object threshold
+     */
+    @ApiModelProperty(
+            value = "The default back pressure object threshold."
+    )
+    public Long getDefaultBackPressureObjectThreshold() {
+        return defaultBackPressureObjectThreshold;
+    }
+
+    public void setDefaultBackPressureObjectThreshold(Long backPressureObjectThreshold) {
+        this.defaultBackPressureObjectThreshold = backPressureObjectThreshold;
+    }
+
+    /**
+     * @return the default back pressure data size threshold
+     */
+    @ApiModelProperty(
+            value = "The default back pressure data size threshold."
+    )
+    public String getDefaultBackPressureDataSizeThreshold() {
+        return defaultBackPressureDataSizeThreshold;
+    }
+
+    public void setDefaultBackPressureDataSizeThreshold(String backPressureDataSizeThreshold) {
+        this.defaultBackPressureDataSizeThreshold = backPressureDataSizeThreshold;
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java
new file mode 100644 (file)
index 0000000..1343400
--- /dev/null
@@ -0,0 +1,700 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.web.dao.impl;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.DataAuthorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.ConnectableType;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.queue.LoadBalanceCompression;
+import org.apache.nifi.controller.queue.LoadBalanceStrategy;
+import org.apache.nifi.connectable.Position;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.exception.ValidationException;
+import org.apache.nifi.controller.queue.DropFlowFileStatus;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.queue.ListFlowFileStatus;
+import org.apache.nifi.controller.repository.ContentNotFoundException;
+import org.apache.nifi.controller.repository.FlowFileRecord;
+import org.apache.nifi.flowfile.FlowFilePrioritizer;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.remote.RemoteGroupPort;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.web.DownloadableContent;
+import org.apache.nifi.web.ResourceNotFoundException;
+import org.apache.nifi.web.api.dto.ConnectableDTO;
+import org.apache.nifi.web.api.dto.ConnectionDTO;
+import org.apache.nifi.web.api.dto.PositionDTO;
+import org.apache.nifi.web.dao.ConnectionDAO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.WebApplicationException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+public class StandardConnectionDAO extends ComponentDAO implements ConnectionDAO {
+
+    private static final Logger logger = LoggerFactory.getLogger(StandardConnectionDAO.class);
+
+    private FlowController flowController;
+    private Authorizer authorizer;
+
+    private Connection locateConnection(final String connectionId) {
+        final ProcessGroup rootGroup = flowController.getFlowManager().getRootGroup();
+        final Connection connection = rootGroup.findConnection(connectionId);
+
+        if (connection == null) {
+            throw new ResourceNotFoundException(String.format("Unable to find connection with id '%s'.", connectionId));
+        } else {
+            return connection;
+        }
+    }
+
+    @Override
+    public boolean hasConnection(String id) {
+        final ProcessGroup rootGroup = flowController.getFlowManager().getRootGroup();
+        return rootGroup.findConnection(id) != null;
+    }
+
+    @Override
+    public Connection getConnection(final String id) {
+        return locateConnection(id);
+    }
+
+    @Override
+    public Set<Connection> getConnections(final String groupId) {
+        final ProcessGroup group = locateProcessGroup(flowController, groupId);
+        return group.getConnections();
+    }
+
+    @Override
+    public DropFlowFileStatus getFlowFileDropRequest(String connectionId, String dropRequestId) {
+        final Connection connection = locateConnection(connectionId);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        final DropFlowFileStatus dropRequest = queue.getDropFlowFileStatus(dropRequestId);
+        if (dropRequest == null) {
+            throw new ResourceNotFoundException(String.format("Unable to find drop request with id '%s'.", dropRequestId));
+        }
+
+        return dropRequest;
+    }
+
+    @Override
+    public ListFlowFileStatus getFlowFileListingRequest(String connectionId, String listingRequestId) {
+        final Connection connection = locateConnection(connectionId);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        final ListFlowFileStatus listRequest = queue.getListFlowFileStatus(listingRequestId);
+        if (listRequest == null) {
+            throw new ResourceNotFoundException(String.format("Unable to find listing request with id '%s'.", listingRequestId));
+        }
+
+        return listRequest;
+    }
+
+    @Override
+    public FlowFileRecord getFlowFile(String id, String flowFileUuid) {
+        try {
+            final Connection connection = locateConnection(id);
+            final FlowFileQueue queue = connection.getFlowFileQueue();
+            final FlowFileRecord flowFile = queue.getFlowFile(flowFileUuid);
+
+            if (flowFile == null) {
+                throw new ResourceNotFoundException(String.format("The FlowFile with UUID %s is no longer in the active queue.", flowFileUuid));
+            }
+
+            // get the attributes and ensure appropriate access
+            final Map<String, String> attributes = flowFile.getAttributes();
+            final Authorizable dataAuthorizable = new DataAuthorizable(connection.getSourceAuthorizable());
+            dataAuthorizable.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser(), attributes);
+
+            return flowFile;
+        } catch (final IOException ioe) {
+            logger.error(String.format("Unable to get the flowfile (%s) at this time.", flowFileUuid), ioe);
+            throw new IllegalStateException("Unable to get the FlowFile at this time.");
+        }
+    }
+
+    /**
+     * Configures the specified connection using the specified dto.
+     */
+    private void configureConnection(Connection connection, ConnectionDTO connectionDTO) {
+        // validate flow file comparators/prioritizers
+        List<FlowFilePrioritizer> newPrioritizers = null;
+        final List<String> prioritizers = connectionDTO.getPrioritizers();
+        if (isNotNull(prioritizers)) {
+            final List<String> newPrioritizersClasses = new ArrayList<>(prioritizers);
+            newPrioritizers = new ArrayList<>();
+            for (final String className : newPrioritizersClasses) {
+                try {
+                    newPrioritizers.add(flowController.getFlowManager().createPrioritizer(className));
+                } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) {
+                    throw new IllegalArgumentException("Unable to set prioritizer " + className + ": " + e);
+                }
+            }
+        }
+
+        // update connection queue
+        if (isNotNull(connectionDTO.getFlowFileExpiration())) {
+            connection.getFlowFileQueue().setFlowFileExpiration(connectionDTO.getFlowFileExpiration());
+        }
+        if (isNotNull(connectionDTO.getBackPressureObjectThreshold())) {
+            connection.getFlowFileQueue().setBackPressureObjectThreshold(connectionDTO.getBackPressureObjectThreshold());
+        }
+        if (isNotNull(connectionDTO.getBackPressureDataSizeThreshold())) {
+            connection.getFlowFileQueue().setBackPressureDataSizeThreshold(connectionDTO.getBackPressureDataSizeThreshold());
+        }
+        if (isNotNull(newPrioritizers)) {
+            connection.getFlowFileQueue().setPriorities(newPrioritizers);
+        }
+
+        final String loadBalanceStrategyName = connectionDTO.getLoadBalanceStrategy();
+        final String loadBalancePartitionAttribute = connectionDTO.getLoadBalancePartitionAttribute();
+        if (isNotNull(loadBalanceStrategyName)) {
+            final LoadBalanceStrategy loadBalanceStrategy = LoadBalanceStrategy.valueOf(loadBalanceStrategyName);
+            connection.getFlowFileQueue().setLoadBalanceStrategy(loadBalanceStrategy, loadBalancePartitionAttribute);
+        }
+
+        final String loadBalanceCompressionName = connectionDTO.getLoadBalanceCompression();
+        if (isNotNull(loadBalanceCompressionName)) {
+            connection.getFlowFileQueue().setLoadBalanceCompression(LoadBalanceCompression.valueOf(loadBalanceCompressionName));
+        }
+
+        // update the connection state
+        if (isNotNull(connectionDTO.getBends())) {
+            final List<Position> bendPoints = new ArrayList<>();
+            for (final PositionDTO bend : connectionDTO.getBends()) {
+                if (bend != null) {
+                    bendPoints.add(new Position(bend.getX(), bend.getY()));
+                }
+            }
+            connection.setBendPoints(bendPoints);
+        }
+        if (isNotNull(connectionDTO.getName())) {
+            connection.setName(connectionDTO.getName());
+        }
+        if (isNotNull(connectionDTO.getLabelIndex())) {
+            connection.setLabelIndex(connectionDTO.getLabelIndex());
+        }
+        if (isNotNull(connectionDTO.getzIndex())) {
+            connection.setZIndex(connectionDTO.getzIndex());
+        }
+    }
+
+    /**
+     * Validates the proposed processor configuration.
+     */
+    private List<String> validateProposedConfiguration(final String groupId, final ConnectionDTO connectionDTO) {
+        List<String> validationErrors = new ArrayList<>();
+
+        if (isNotNull(connectionDTO.getBackPressureObjectThreshold()) && connectionDTO.getBackPressureObjectThreshold() < 0) {
+            validationErrors.add("Max queue size must be a non-negative integer");
+        }
+        if (isNotNull(connectionDTO.getFlowFileExpiration())) {
+            Matcher expirationMatcher = FormatUtils.TIME_DURATION_PATTERN.matcher(connectionDTO.getFlowFileExpiration());
+            if (!expirationMatcher.matches()) {
+                validationErrors.add("Flow file expiration is not a valid time duration (ie 30 sec, 5 min)");
+            }
+        }
+        if (isNotNull(connectionDTO.getLabelIndex())) {
+            if (connectionDTO.getLabelIndex() < 0) {
+                validationErrors.add("The label index must be positive.");
+            }
+        }
+
+        // validation is required when connecting to a remote process group since each node in a
+        // cluster may or may not be authorized
+        final ConnectableDTO proposedDestination = connectionDTO.getDestination();
+        if (proposedDestination != null && ConnectableType.REMOTE_INPUT_PORT.name().equals(proposedDestination.getType())) {
+            // the group id must be specified
+            if (proposedDestination.getGroupId() == null) {
+                validationErrors.add("When the destination is a remote input port its group id is required.");
+                return validationErrors;
+            }
+
+            // attempt to location the proprosed destination
+            final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, groupId);
+            final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(proposedDestination.getGroupId());
+            if (remoteProcessGroup == null) {
+                validationErrors.add("Unable to find the specified remote process group.");
+                return validationErrors;
+            }
+
+            // ensure the new destination was found
+            final RemoteGroupPort remoteInputPort = remoteProcessGroup.getInputPort(proposedDestination.getId());
+            if (remoteInputPort == null) {
+                validationErrors.add("Unable to find the specified destination.");
+                return validationErrors;
+            }
+        }
+
+        return validationErrors;
+    }
+
+    @Override
+    public Connection createConnection(final String groupId, final ConnectionDTO connectionDTO) {
+        final ProcessGroup group = locateProcessGroup(flowController, groupId);
+
+        if (isNotNull(connectionDTO.getParentGroupId()) && !flowController.getFlowManager().areGroupsSame(connectionDTO.getParentGroupId(), groupId)) {
+            throw new IllegalStateException("Cannot specify a different Parent Group ID than the Group to which the Connection is being added");
+        }
+
+        // get the source and destination connectables
+        final ConnectableDTO sourceConnectableDTO = connectionDTO.getSource();
+        final ConnectableDTO destinationConnectableDTO = connectionDTO.getDestination();
+
+        // ensure both are specified
+        if (sourceConnectableDTO == null || destinationConnectableDTO == null) {
+            throw new IllegalArgumentException("Both source and destinations must be specified.");
+        }
+
+        // if the source/destination connectable's group id has not been set, its inferred to be the current group
+        if (sourceConnectableDTO.getGroupId() == null) {
+            sourceConnectableDTO.setGroupId(groupId);
+        }
+        if (destinationConnectableDTO.getGroupId() == null) {
+            destinationConnectableDTO.setGroupId(groupId);
+        }
+
+        // validate the proposed configuration
+        final List<String> validationErrors = validateProposedConfiguration(groupId, connectionDTO);
+
+        // ensure there was no validation errors
+        if (!validationErrors.isEmpty()) {
+            throw new ValidationException(validationErrors);
+        }
+
+        // find the source
+        final Connectable source;
+        if (ConnectableType.REMOTE_OUTPUT_PORT.name().equals(sourceConnectableDTO.getType())) {
+            final ProcessGroup sourceParentGroup = locateProcessGroup(flowController, groupId);
+            final RemoteProcessGroup remoteProcessGroup = sourceParentGroup.getRemoteProcessGroup(sourceConnectableDTO.getGroupId());
+            source = remoteProcessGroup.getOutputPort(sourceConnectableDTO.getId());
+        } else {
+            final ProcessGroup sourceGroup = locateProcessGroup(flowController, sourceConnectableDTO.getGroupId());
+            source = sourceGroup.getConnectable(sourceConnectableDTO.getId());
+        }
+
+        // find the destination
+        final Connectable destination;
+        if (ConnectableType.REMOTE_INPUT_PORT.name().equals(destinationConnectableDTO.getType())) {
+            final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, groupId);
+            final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(destinationConnectableDTO.getGroupId());
+            destination = remoteProcessGroup.getInputPort(destinationConnectableDTO.getId());
+        } else {
+            final ProcessGroup destinationGroup = locateProcessGroup(flowController, destinationConnectableDTO.getGroupId());
+            destination = destinationGroup.getConnectable(destinationConnectableDTO.getId());
+        }
+
+        // determine the relationships
+        final Set<String> relationships = new HashSet<>();
+        if (isNotNull(connectionDTO.getSelectedRelationships())) {
+            relationships.addAll(connectionDTO.getSelectedRelationships());
+        }
+
+        // create the connection
+        final Connection connection = flowController.createConnection(connectionDTO.getId(), connectionDTO.getName(), source, destination, relationships);
+
+        // configure the connection
+        configureConnection(connection, connectionDTO);
+
+        // add the connection to the group
+        group.addConnection(connection);
+        return connection;
+    }
+
+    @Override
+    public DropFlowFileStatus createFlowFileDropRequest(String id, String dropRequestId) {
+        final Connection connection = locateConnection(id);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new WebApplicationException(new Throwable("Unable to access details for current user."));
+        }
+
+        return queue.dropFlowFiles(dropRequestId, user.getIdentity());
+    }
+
+    @Override
+    public ListFlowFileStatus createFlowFileListingRequest(String id, String listingRequestId) {
+        final Connection connection = locateConnection(id);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        // ensure we can list
+        verifyList(queue);
+
+        return queue.listFlowFiles(listingRequestId, 100);
+    }
+
+    @Override
+    public void verifyCreate(String groupId, ConnectionDTO connectionDTO) {
+        // validate the incoming request
+        final List<String> validationErrors = validateProposedConfiguration(groupId, connectionDTO);
+
+        // ensure there was no validation errors
+        if (!validationErrors.isEmpty()) {
+            throw new ValidationException(validationErrors);
+        }
+
+        // Ensure that both the source and the destination for the connection exist.
+        // In the case that the source or destination is a port in a Remote Process Group,
+        // this is necessary because the ports can change in the background. It may still be
+        // possible for a port to disappear between the 'verify' stage and the creation stage,
+        // but this prevents the case where some nodes already know about the port while other
+        // nodes in the cluster do not. This is a more common case, as users may try to connect
+        // to the port as soon as the port is created.
+        final ConnectableDTO sourceDto = connectionDTO.getSource();
+        if (sourceDto == null || sourceDto.getId() == null) {
+            throw new IllegalArgumentException("Cannot create connection without specifying source");
+        }
+
+        final ConnectableDTO destinationDto = connectionDTO.getDestination();
+        if (destinationDto == null || destinationDto.getId() == null) {
+            throw new IllegalArgumentException("Cannot create connection without specifying destination");
+        }
+
+        if (ConnectableType.REMOTE_OUTPUT_PORT.name().equals(sourceDto.getType())) {
+            final ProcessGroup sourceParentGroup = locateProcessGroup(flowController, groupId);
+
+            final RemoteProcessGroup remoteProcessGroup = sourceParentGroup.getRemoteProcessGroup(sourceDto.getGroupId());
+            if (remoteProcessGroup == null) {
+                throw new IllegalArgumentException("Unable to find the specified remote process group.");
+            }
+
+            final RemoteGroupPort sourceConnectable = remoteProcessGroup.getOutputPort(sourceDto.getId());
+            if (sourceConnectable == null) {
+                throw new IllegalArgumentException("The specified source for the connection does not exist");
+            } else if (!sourceConnectable.getTargetExists()) {
+                throw new IllegalArgumentException("The specified remote output port does not exist.");
+            }
+        } else {
+            final ProcessGroup sourceGroup = locateProcessGroup(flowController, sourceDto.getGroupId());
+            final Connectable sourceConnectable = sourceGroup.getConnectable(sourceDto.getId());
+            if (sourceConnectable == null) {
+                throw new IllegalArgumentException("The specified source for the connection does not exist");
+            }
+        }
+
+        if (ConnectableType.REMOTE_INPUT_PORT.name().equals(destinationDto.getType())) {
+            final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, groupId);
+
+            final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(destinationDto.getGroupId());
+            if (remoteProcessGroup == null) {
+                throw new IllegalArgumentException("Unable to find the specified remote process group.");
+            }
+
+            final RemoteGroupPort destinationConnectable = remoteProcessGroup.getInputPort(destinationDto.getId());
+            if (destinationConnectable == null) {
+                throw new IllegalArgumentException("The specified destination for the connection does not exist");
+            } else if (!destinationConnectable.getTargetExists()) {
+                throw new IllegalArgumentException("The specified remote input port does not exist.");
+            }
+        } else {
+            final ProcessGroup destinationGroup = locateProcessGroup(flowController, destinationDto.getGroupId());
+            final Connectable destinationConnectable = destinationGroup.getConnectable(destinationDto.getId());
+            if (destinationConnectable == null) {
+                throw new IllegalArgumentException("The specified destination for the connection does not exist");
+            }
+        }
+    }
+
+    private void verifyList(final FlowFileQueue queue) {
+        queue.verifyCanList();
+    }
+
+    @Override
+    public void verifyList(String id) {
+        final Connection connection = locateConnection(id);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+        verifyList(queue);
+    }
+
+    @Override
+    public void verifyUpdate(ConnectionDTO connectionDTO) {
+        verifyUpdate(locateConnection(connectionDTO.getId()), connectionDTO);
+    }
+
+    private void verifyUpdate(final Connection connection, final ConnectionDTO connectionDTO) {
+        // determine what the request is attempting
+        if (isAnyNotNull(connectionDTO.getBackPressureDataSizeThreshold(),
+                connectionDTO.getBackPressureObjectThreshold(),
+                connectionDTO.getDestination(),
+                connectionDTO.getFlowFileExpiration(),
+                connectionDTO.getName(),
+                connectionDTO.getPosition(),
+                connectionDTO.getPrioritizers(),
+                connectionDTO.getSelectedRelationships())) {
+
+            // validate the incoming request
+            final List<String> validationErrors = validateProposedConfiguration(connection.getProcessGroup().getIdentifier(), connectionDTO);
+
+            // ensure there was no validation errors
+            if (!validationErrors.isEmpty()) {
+                throw new ValidationException(validationErrors);
+            }
+
+            // If destination is changing, ensure that current destination is not running. This check is done here, rather than
+            // in the Connection object itself because the Connection object itself does not know which updates are to occur and
+            // we don't want to prevent updating things like the connection name or backpressure just because the destination is running
+            final Connectable destination = connection.getDestination();
+            if (destination != null && destination.isRunning() && destination.getConnectableType() != ConnectableType.FUNNEL && destination.getConnectableType() != ConnectableType.INPUT_PORT) {
+                throw new ValidationException(Collections.singletonList("Cannot change the destination of connection because the current destination is running"));
+            }
+
+            // verify that this connection supports modification
+            connection.verifyCanUpdate();
+        }
+    }
+
+    @Override
+    public Connection updateConnection(final ConnectionDTO connectionDTO) {
+        final Connection connection = locateConnection(connectionDTO.getId());
+        final ProcessGroup group = connection.getProcessGroup();
+
+        // ensure we can update
+        verifyUpdate(connection, connectionDTO);
+
+        final Collection<Relationship> newProcessorRelationships = new ArrayList<>();
+        Connectable newDestination = null;
+
+        // ensure that the source ID is correct, if specified.
+        final Connectable existingSource = connection.getSource();
+        if (isNotNull(connectionDTO.getSource()) && !existingSource.getIdentifier().equals(connectionDTO.getSource().getId())) {
+            throw new IllegalStateException("Connection with ID " + connectionDTO.getId() + " has conflicting Source ID");
+        }
+
+        // determine if the destination changed
+        final ConnectableDTO proposedDestination = connectionDTO.getDestination();
+        if (proposedDestination != null) {
+            final Connectable currentDestination = connection.getDestination();
+
+            // handle remote input port differently
+            if (ConnectableType.REMOTE_INPUT_PORT.name().equals(proposedDestination.getType())) {
+                // the group id must be specified
+                if (proposedDestination.getGroupId() == null) {
+                    throw new IllegalArgumentException("When the destination is a remote input port its group id is required.");
+                }
+
+                // if the current destination is a remote input port
+                boolean isDifferentRemoteProcessGroup = false;
+                if (currentDestination.getConnectableType() == ConnectableType.REMOTE_INPUT_PORT) {
+                    RemoteGroupPort remotePort = (RemoteGroupPort) currentDestination;
+                    if (!proposedDestination.getGroupId().equals(remotePort.getRemoteProcessGroup().getIdentifier())) {
+                        isDifferentRemoteProcessGroup = true;
+                    }
+                }
+
+                // if the destination is changing or the previous destination was a different remote process group
+                if (!proposedDestination.getId().equals(currentDestination.getIdentifier()) || isDifferentRemoteProcessGroup) {
+                    final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, group.getIdentifier());
+                    final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(proposedDestination.getGroupId());
+
+                    // ensure the remote process group was found
+                    if (remoteProcessGroup == null) {
+                        throw new IllegalArgumentException("Unable to find the specified remote process group.");
+                    }
+
+                    final RemoteGroupPort remoteInputPort = remoteProcessGroup.getInputPort(proposedDestination.getId());
+
+                    // ensure the new destination was found
+                    if (remoteInputPort == null) {
+                        throw new IllegalArgumentException("Unable to find the specified destination.");
+                    }
+
+                    // ensure the remote port actually exists
+                    if (!remoteInputPort.getTargetExists()) {
+                        throw new IllegalArgumentException("The specified remote input port does not exist.");
+                    } else {
+                        newDestination = remoteInputPort;
+                    }
+                }
+            } else {
+                // if there is a different destination id
+                if (!proposedDestination.getId().equals(currentDestination.getIdentifier())) {
+                    // if the destination connectable's group id has not been set, its inferred to be the current group
+                    if (proposedDestination.getGroupId() == null) {
+                        proposedDestination.setGroupId(group.getIdentifier());
+                    }
+
+                    final ProcessGroup destinationGroup = locateProcessGroup(flowController, proposedDestination.getGroupId());
+                    newDestination = destinationGroup.getConnectable(proposedDestination.getId());
+
+                    // ensure the new destination was found
+                    if (newDestination == null) {
+                        throw new IllegalArgumentException("Unable to find the specified destination.");
+                    }
+                }
+            }
+        }
+
+        // determine any new relationships
+        final Set<String> relationships = connectionDTO.getSelectedRelationships();
+        if (isNotNull(relationships)) {
+            if (relationships.isEmpty()) {
+                throw new IllegalArgumentException("Cannot remove all relationships from Connection with ID " + connection.getIdentifier() + " -- remove the Connection instead");
+            }
+            if (existingSource == null) {
+                throw new IllegalArgumentException("Cannot specify new relationships without including the source.");
+            }
+
+            final Connectable destination = newDestination == null ? connection.getDestination() : newDestination;
+
+            for (final String relationship : relationships) {
+                int prevSize = newProcessorRelationships.size();
+
+                final Relationship processorRelationshipSource = existingSource.getRelationship(relationship);
+
+                if (processorRelationshipSource != null) {
+                    newProcessorRelationships.add(processorRelationshipSource);
+                }
+
+                final Relationship processorRelationshipDest = destination.getRelationship(relationship);
+
+                if (processorRelationshipDest != null) {
+                    newProcessorRelationships.add(processorRelationshipDest);
+                }
+
+                if (newProcessorRelationships.size() == prevSize) {
+                    throw new IllegalArgumentException("Unable to locate " + relationship + " relationship.");
+                }
+            }
+        }
+
+        // configure the connection
+        configureConnection(connection, connectionDTO);
+        group.onComponentModified();
+
+        // update the relationships if necessary
+        if (!newProcessorRelationships.isEmpty()) {
+            connection.setRelationships(newProcessorRelationships);
+        }
+
+        // update the destination if necessary
+        if (isNotNull(newDestination)) {
+            connection.setDestination(newDestination);
+        }
+
+        return connection;
+    }
+
+    @Override
+    public void verifyDelete(String id) {
+        final Connection connection = locateConnection(id);
+        connection.verifyCanDelete();
+    }
+
+    @Override
+    public void deleteConnection(final String id) {
+        final Connection connection = locateConnection(id);
+        connection.getProcessGroup().removeConnection(connection);
+    }
+
+    @Override
+    public DropFlowFileStatus deleteFlowFileDropRequest(String connectionId, String dropRequestId) {
+        final Connection connection = locateConnection(connectionId);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        final DropFlowFileStatus dropFlowFileStatus = queue.cancelDropFlowFileRequest(dropRequestId);
+        if (dropFlowFileStatus == null) {
+            throw new ResourceNotFoundException(String.format("Unable to find drop request with id '%s'.", dropRequestId));
+        }
+
+        return dropFlowFileStatus;
+    }
+
+    @Override
+    public ListFlowFileStatus deleteFlowFileListingRequest(String connectionId, String listingRequestId) {
+        final Connection connection = locateConnection(connectionId);
+        final FlowFileQueue queue = connection.getFlowFileQueue();
+
+        final ListFlowFileStatus listFlowFileStatus = queue.cancelListFlowFileRequest(listingRequestId);
+        if (listFlowFileStatus == null) {
+            throw new ResourceNotFoundException(String.format("Unable to find listing request with id '%s'.", listingRequestId));
+        }
+
+        return listFlowFileStatus;
+    }
+
+    @Override
+    public DownloadableContent getContent(String id, String flowFileUuid, String requestUri) {
+        try {
+            final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+            final Connection connection = locateConnection(id);
+            final FlowFileQueue queue = connection.getFlowFileQueue();
+            final FlowFileRecord flowFile = queue.getFlowFile(flowFileUuid);
+
+            if (flowFile == null) {
+                throw new ResourceNotFoundException(String.format("The FlowFile with UUID %s is no longer in the active queue.", flowFileUuid));
+            }
+
+            // get the attributes and ensure appropriate access
+            final Map<String, String> attributes = flowFile.getAttributes();
+            final Authorizable dataAuthorizable = new DataAuthorizable(connection.getSourceAuthorizable());
+            dataAuthorizable.authorize(authorizer, RequestAction.READ, user, attributes);
+
+            // get the filename and fall back to the identifier (should never happen)
+            String filename = attributes.get(CoreAttributes.FILENAME.key());
+            if (filename == null) {
+                filename = flowFileUuid;
+            }
+
+            // get the mime-type
+            final String type = attributes.get(CoreAttributes.MIME_TYPE.key());
+
+            // get the content
+            final InputStream content = flowController.getContent(flowFile, user.getIdentity(), requestUri);
+            return new DownloadableContent(filename, type, content);
+        } catch (final ContentNotFoundException cnfe) {
+            throw new ResourceNotFoundException("Unable to find the specified content.");
+        } catch (final IOException ioe) {
+            logger.error(String.format("Unable to get the content for flowfile (%s) at this time.", flowFileUuid), ioe);
+            throw new IllegalStateException("Unable to get the content at this time.");
+        }
+    }
+
+    /* setters */
+    public void setFlowController(final FlowController flowController) {
+        this.flowController = flowController;
+    }
+
+    public void setAuthorizer(Authorizer authorizer) {
+        this.authorizer = authorizer;
+    }
+}
diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java
new file mode 100644 (file)
index 0000000..a3a1840
--- /dev/null
@@ -0,0 +1,1226 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+package org.apache.nifi.web.server;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.URI;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.ServletContext;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.NiFiServer;
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleDetails;
+import org.apache.nifi.controller.UninheritableFlowException;
+import org.apache.nifi.controller.serialization.FlowSerializationException;
+import org.apache.nifi.controller.serialization.FlowSynchronizationException;
+import org.apache.nifi.documentation.DocGenerator;
+import org.apache.nifi.lifecycle.LifeCycleStartException;
+import org.apache.nifi.nar.ExtensionDiscoveringManager;
+import org.apache.nifi.nar.ExtensionManagerHolder;
+import org.apache.nifi.nar.ExtensionMapping;
+import org.apache.nifi.nar.ExtensionUiLoader;
+import org.apache.nifi.nar.NarAutoLoader;
+import org.apache.nifi.nar.DCAEAutoLoader;
+import org.apache.nifi.nar.NarClassLoadersHolder;
+import org.apache.nifi.nar.NarLoader;
+import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
+import org.apache.nifi.nar.StandardNarLoader;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.services.FlowService;
+import org.apache.nifi.ui.extension.UiExtension;
+import org.apache.nifi.ui.extension.UiExtensionMapping;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.ContentAccess;
+import org.apache.nifi.web.NiFiWebConfigurationContext;
+import org.apache.nifi.web.UiExtensionType;
+import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
+import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
+import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
+import org.apache.nifi.web.security.headers.XSSProtectionFilter;
+import org.eclipse.jetty.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.deploy.App;
+import org.eclipse.jetty.deploy.DeploymentManager;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.server.handler.gzip.GzipHandler;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
+import org.eclipse.jetty.webapp.WebAppClassLoader;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+/**
+ * Encapsulates the Jetty instance.
+ */
+public class JettyServer implements NiFiServer, ExtensionUiLoader {
+
+    private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
+    private static final String WEB_DEFAULTS_XML = "org/apache/nifi/web/webdefault.xml";
+
+    private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
+    private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$";
+
+    private static final FileFilter WAR_FILTER = new FileFilter() {
+        @Override
+        public boolean accept(File pathname) {
+            final String nameToTest = pathname.getName().toLowerCase();
+            return nameToTest.endsWith(".war") && pathname.isFile();
+        }
+    };
+
+    private final Server server;
+    private final NiFiProperties props;
+
+    private Bundle systemBundle;
+    private Set<Bundle> bundles;
+    private ExtensionMapping extensionMapping;
+    private NarAutoLoader narAutoLoader;
+    private DCAEAutoLoader dcaeAutoLoader;
+
+    private WebAppContext webApiContext;
+    private WebAppContext webDocsContext;
+
+    // content viewer and mime type specific extensions
+    private WebAppContext webContentViewerContext;
+    private Collection<WebAppContext> contentViewerWebContexts;
+
+    // component (processor, controller service, reporting task) ui extensions
+    private UiExtensionMapping componentUiExtensions;
+    private Collection<WebAppContext> componentUiExtensionWebContexts;
+
+    private DeploymentManager deploymentManager;
+
+    public JettyServer(final NiFiProperties props, final Set<Bundle> bundles) {
+        final QueuedThreadPool threadPool = new QueuedThreadPool(props.getWebThreads());
+        threadPool.setName("NiFi Web Server");
+
+        // create the server
+        this.server = new Server(threadPool);
+        this.props = props;
+
+        // enable the annotation based configuration to ensure the jsp container is initialized properly
+        final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
+        classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName());
+
+        // configure server
+        configureConnectors(server);
+
+        // load wars from the bundle
+        final Handler warHandlers = loadInitialWars(bundles);
+
+        final HandlerList allHandlers = new HandlerList();
+
+        // Only restrict the host header if running in HTTPS mode
+        if (props.isHTTPSConfigured()) {
+            // Create a handler for the host header and add it to the server
+            HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
+            logger.info("Created HostHeaderHandler [" + hostHeaderHandler.toString() + "]");
+
+            // Add this before the WAR handlers
+            allHandlers.addHandler(hostHeaderHandler);
+        } else {
+            logger.info("Running in HTTP mode; host headers not restricted");
+        }
+
+
+        final ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
+        contextHandlers.addHandler(warHandlers);
+        allHandlers.addHandler(contextHandlers);
+        server.setHandler(allHandlers);
+
+        deploymentManager = new DeploymentManager();
+        deploymentManager.setContextAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
+        deploymentManager.setContexts(contextHandlers);
+        server.addBean(deploymentManager);
+    }
+
+    /**
+     * Instantiates this object but does not perform any configuration. Used for unit testing.
+     */
+     JettyServer(Server server, NiFiProperties properties) {
+        this.server = server;
+        this.props = properties;
+    }
+
+    private Handler loadInitialWars(final Set<Bundle> bundles) {
+
+        // load WARs
+        final Map<File, Bundle> warToBundleLookup = findWars(bundles);
+
+        // locate each war being deployed
+        File webUiWar = null;
+        File webApiWar = null;
+        File webErrorWar = null;
+        File webDocsWar = null;
+        File webContentViewerWar = null;
+        Map<File, Bundle> otherWars = new HashMap<>();
+        for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
+            final File war = warBundleEntry.getKey();
+            final Bundle warBundle = warBundleEntry.getValue();
+
+            if (war.getName().toLowerCase().startsWith("nifi-web-api")) {
+                webApiWar = war;
+            } else if (war.getName().toLowerCase().startsWith("nifi-web-error")) {
+                webErrorWar = war;
+            } else if (war.getName().toLowerCase().startsWith("nifi-web-docs")) {
+                webDocsWar = war;
+            } else if (war.getName().toLowerCase().startsWith("nifi-web-content-viewer")) {
+                webContentViewerWar = war;
+            } else if (war.getName().toLowerCase().startsWith("nifi-web")) {
+                webUiWar = war;
+            } else {
+                otherWars.put(war, warBundle);
+            }
+        }
+
+        // ensure the required wars were found
+        if (webUiWar == null) {
+            throw new RuntimeException("Unable to load nifi-web WAR");
+        } else if (webApiWar == null) {
+            throw new RuntimeException("Unable to load nifi-web-api WAR");
+        } else if (webDocsWar == null) {
+            throw new RuntimeException("Unable to load nifi-web-docs WAR");
+        } else if (webErrorWar == null) {
+            throw new RuntimeException("Unable to load nifi-web-error WAR");
+        } else if (webContentViewerWar == null) {
+            throw new RuntimeException("Unable to load nifi-web-content-viewer WAR");
+        }
+
+        // handlers for each war and init params for the web api
+        final ExtensionUiInfo extensionUiInfo = loadWars(otherWars);
+        componentUiExtensionWebContexts = new ArrayList<>(extensionUiInfo.getComponentUiExtensionWebContexts());
+        contentViewerWebContexts = new ArrayList<>(extensionUiInfo.getContentViewerWebContexts());
+        componentUiExtensions = new UiExtensionMapping(extensionUiInfo.getComponentUiExtensionsByType());
+
+        final HandlerCollection webAppContextHandlers = new HandlerCollection();
+        final Collection<WebAppContext> extensionUiContexts = extensionUiInfo.getWebAppContexts();
+        extensionUiContexts.stream().forEach(c -> webAppContextHandlers.addHandler(c));
+
+        final ClassLoader frameworkClassLoader = getClass().getClassLoader();
+
+        // load the web ui app
+        final WebAppContext webUiContext = loadWar(webUiWar, "/nifi", frameworkClassLoader);
+        webUiContext.getInitParams().put("oidc-supported", String.valueOf(props.isOidcEnabled()));
+        webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled()));
+        webUiContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
+        webAppContextHandlers.addHandler(webUiContext);
+
+        // load the web api app
+        webApiContext = loadWar(webApiWar, "/nifi-api", frameworkClassLoader);
+        webAppContextHandlers.addHandler(webApiContext);
+
+        // load the content viewer app
+        webContentViewerContext = loadWar(webContentViewerWar, "/nifi-content-viewer", frameworkClassLoader);
+        webContentViewerContext.getInitParams().putAll(extensionUiInfo.getMimeMappings());
+        webAppContextHandlers.addHandler(webContentViewerContext);
+
+        // create a web app for the docs
+        final String docsContextPath = "/nifi-docs";
+
+        // load the documentation war
+        webDocsContext = loadWar(webDocsWar, docsContextPath, frameworkClassLoader);
+
+        // add the servlets which serve the HTML documentation within the documentation web app
+        addDocsServlets(webDocsContext);
+
+        webAppContextHandlers.addHandler(webDocsContext);
+
+        // load the web error app
+        final WebAppContext webErrorContext = loadWar(webErrorWar, "/", frameworkClassLoader);
+        webErrorContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
+        webAppContextHandlers.addHandler(webErrorContext);
+
+        // deploy the web apps
+        return gzip(webAppContextHandlers);
+    }
+
+    @Override
+    public void loadExtensionUis(final Set<Bundle> bundles) {
+         // Find and load any WARs contained within the set of bundles...
+        final Map<File, Bundle> warToBundleLookup = findWars(bundles);
+        final ExtensionUiInfo extensionUiInfo = loadWars(warToBundleLookup);
+
+        final Collection<WebAppContext> webAppContexts = extensionUiInfo.getWebAppContexts();
+        if (CollectionUtils.isEmpty(webAppContexts)) {
+            logger.debug("No webapp contexts were loaded, returning...");
+            return;
+        }
+
+        // Deploy each WAR that was loaded...
+        for (final WebAppContext webAppContext : webAppContexts) {
+            final App extensionUiApp = new App(deploymentManager, null, "nifi-jetty-server", webAppContext);
+            deploymentManager.addApp(extensionUiApp);
+        }
+
+        final Collection<WebAppContext> componentUiExtensionWebContexts = extensionUiInfo.getComponentUiExtensionWebContexts();
+        final Collection<WebAppContext> contentViewerWebContexts = extensionUiInfo.getContentViewerWebContexts();
+
+        // Inject the configuration context and security filter into contexts that need it
+        final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
+        final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
+        final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
+        final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
+
+        performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
+        performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
+
+        // Merge results of current loading into previously loaded results...
+        this.componentUiExtensionWebContexts.addAll(componentUiExtensionWebContexts);
+        this.contentViewerWebContexts.addAll(contentViewerWebContexts);
+        this.componentUiExtensions.addUiExtensions(extensionUiInfo.getComponentUiExtensionsByType());
+
+        for (final WebAppContext webAppContext : webAppContexts) {
+            final Throwable t = webAppContext.getUnavailableException();
+            if (t != null) {
+                logger.error("Unable to start context due to " + t.getMessage(), t);
+            }
+        }
+    }
+
+    private ExtensionUiInfo loadWars(final Map<File, Bundle> warToBundleLookup) {
+         // handlers for each war and init params for the web api
+        final List<WebAppContext> webAppContexts = new ArrayList<>();
+        final Map<String, String> mimeMappings = new HashMap<>();
+        final Collection<WebAppContext> componentUiExtensionWebContexts = new ArrayList<>();
+        final Collection<WebAppContext> contentViewerWebContexts = new ArrayList<>();
+        final Map<String, List<UiExtension>> componentUiExtensionsByType = new HashMap<>();
+
+        final ClassLoader frameworkClassLoader = getClass().getClassLoader();
+        final ClassLoader jettyClassLoader = frameworkClassLoader.getParent();
+
+        // deploy the other wars
+        if (!warToBundleLookup.isEmpty()) {
+            // ui extension organized by component type
+            for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
+                final File war = warBundleEntry.getKey();
+                final Bundle warBundle = warBundleEntry.getValue();
+
+                // identify all known extension types in the war
+                final Map<UiExtensionType, List<String>> uiExtensionInWar = new HashMap<>();
+                identifyUiExtensionsForComponents(uiExtensionInWar, war);
+
+                // only include wars that are for custom processor ui's
+                if (!uiExtensionInWar.isEmpty()) {
+                    // get the context path
+                    String warName = StringUtils.substringBeforeLast(war.getName(), ".");
+                    String warContextPath = String.format("/%s", warName);
+
+                    // get the classloader for this war
+                    ClassLoader narClassLoaderForWar = warBundle.getClassLoader();
+
+                    // this should never be null
+                    if (narClassLoaderForWar == null) {
+                        narClassLoaderForWar = jettyClassLoader;
+                    }
+
+                    // create the extension web app context
+                    WebAppContext extensionUiContext = loadWar(war, warContextPath, narClassLoaderForWar);
+
+                    // create the ui extensions
+                    for (final Map.Entry<UiExtensionType, List<String>> entry : uiExtensionInWar.entrySet()) {
+                        final UiExtensionType extensionType = entry.getKey();
+                        final List<String> types = entry.getValue();
+
+                        if (UiExtensionType.ContentViewer.equals(extensionType)) {
+                            // consider each content type identified
+                            for (final String contentType : types) {
+                                // map the content type to the context path
+                                mimeMappings.put(contentType, warContextPath);
+                            }
+
+                            // this ui extension provides a content viewer
+                            contentViewerWebContexts.add(extensionUiContext);
+                        } else {
+                            // consider each component type identified
+                            for (final String componentTypeCoordinates : types) {
+                                logger.info(String.format("Loading UI extension [%s, %s] for %s", extensionType, warContextPath, componentTypeCoordinates));
+
+                                // record the extension definition
+                                final UiExtension uiExtension = new UiExtension(extensionType, warContextPath);
+
+                                // create if this is the first extension for this component type
+                                List<UiExtension> componentUiExtensionsForType = componentUiExtensionsByType.get(componentTypeCoordinates);
+                                if (componentUiExtensionsForType == null) {
+                                    componentUiExtensionsForType = new ArrayList<>();
+                                    componentUiExtensionsByType.put(componentTypeCoordinates, componentUiExtensionsForType);
+                                }
+
+                                // see if there is already a ui extension of this same time
+                                if (containsUiExtensionType(componentUiExtensionsForType, extensionType)) {
+                                    throw new IllegalStateException(String.format("Encountered duplicate UI for %s", componentTypeCoordinates));
+                                }
+
+                                // record this extension
+                                componentUiExtensionsForType.add(uiExtension);
+                            }
+
+                            // this ui extension provides a component custom ui
+                            componentUiExtensionWebContexts.add(extensionUiContext);
+                        }
+                    }
+
+                    // include custom ui web context in the handlers
+                    webAppContexts.add(extensionUiContext);
+                }
+            }
+        }
+
+        return new ExtensionUiInfo(webAppContexts, mimeMappings, componentUiExtensionWebContexts, contentViewerWebContexts, componentUiExtensionsByType);
+    }
+
+    /**
+     * Returns whether or not the specified ui extensions already contains an extension of the specified type.
+     *
+     * @param componentUiExtensionsForType ui extensions for the type
+     * @param extensionType                type of ui extension
+     * @return whether or not the specified ui extensions already contains an extension of the specified type
+     */
+    private boolean containsUiExtensionType(final List<UiExtension> componentUiExtensionsForType, final UiExtensionType extensionType) {
+        for (final UiExtension uiExtension : componentUiExtensionsForType) {
+            if (extensionType.equals(uiExtension.getExtensionType())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Enables compression for the specified handler.
+     *
+     * @param handler handler to enable compression for
+     * @return compression enabled handler
+     */
+    private Handler gzip(final Handler handler) {
+        final GzipHandler gzip = new GzipHandler();
+        gzip.setIncludedMethods("GET", "POST", "PUT", "DELETE");
+        gzip.setHandler(handler);
+        return gzip;
+    }
+
+    private Map<File, Bundle> findWars(final Set<Bundle> bundles) {
+        final Map<File, Bundle> wars = new HashMap<>();
+
+        // consider each nar working directory
+        bundles.forEach(bundle -> {
+            final BundleDetails details = bundle.getBundleDetails();
+            final File narDependencies = new File(details.getWorkingDirectory(), "NAR-INF/bundled-dependencies");
+            if (narDependencies.isDirectory()) {
+                // list the wars from this nar
+                final File[] narDependencyDirs = narDependencies.listFiles(WAR_FILTER);
+                if (narDependencyDirs == null) {
+                    throw new IllegalStateException(String.format("Unable to access working directory for NAR dependencies in: %s", narDependencies.getAbsolutePath()));
+                }
+
+                // add each war
+                for (final File war : narDependencyDirs) {
+                    wars.put(war, bundle);
+                }
+            }
+        });
+
+        return wars;
+    }
+
+    private void readUiExtensions(final Map<UiExtensionType, List<String>> uiExtensions, final UiExtensionType uiExtensionType, final JarFile jarFile, final JarEntry jarEntry) throws IOException {
+        if (jarEntry == null) {
+            return;
+        }
+
+        // get an input stream for the nifi-processor configuration file
+        try (BufferedReader in = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry)))) {
+
+            // read in each configured type
+            String rawComponentType;
+            while ((rawComponentType = in.readLine()) != null) {
+                // extract the component type
+                final String componentType = extractComponentType(rawComponentType);
+                if (componentType != null) {
+                    List<String> extensions = uiExtensions.get(uiExtensionType);
+
+                    // if there are currently no extensions for this type create it
+                    if (extensions == null) {
+                        extensions = new ArrayList<>();
+                        uiExtensions.put(uiExtensionType, extensions);
+                    }
+
+                    // add the specified type
+                    extensions.add(componentType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Identifies all known UI extensions and stores them in the specified map.
+     *
+     * @param uiExtensions extensions
+     * @param warFile      war
+     */
+    private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) {
+        try (final JarFile jarFile = new JarFile(warFile)) {
+            // locate the ui extensions
+            readUiExtensions(uiExtensions, UiExtensionType.ContentViewer, jarFile, jarFile.getJarEntry("META-INF/nifi-content-viewer"));
+            readUiExtensions(uiExtensions, UiExtensionType.ProcessorConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-processor-configuration"));
+            readUiExtensions(uiExtensions, UiExtensionType.ControllerServiceConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-controller-service-configuration"));
+            readUiExtensions(uiExtensions, UiExtensionType.ReportingTaskConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-reporting-task-configuration"));
+        } catch (IOException ioe) {
+            logger.warn(String.format("Unable to inspect %s for a UI extensions.", warFile));
+        }
+    }
+
+    /**
+     * Extracts the component type. Trims the line and considers comments.
+     * Returns null if no type was found.
+     *
+     * @param line line
+     * @return type
+     */
+    private String extractComponentType(final String line) {
+        final String trimmedLine = line.trim();
+        if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) {
+            final int indexOfPound = trimmedLine.indexOf("#");
+            return (indexOfPound > 0) ? trimmedLine.substring(0, indexOfPound) : trimmedLine;
+        }
+        return null;
+    }
+
+    private WebAppContext loadWar(final File warFile, final String contextPath, final ClassLoader parentClassLoader) {
+        final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
+        webappContext.setContextPath(contextPath);
+        webappContext.setDisplayName(contextPath);
+
+        // instruction jetty to examine these jars for tlds, web-fragments, etc
+        webappContext.setAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
+
+        // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib
+        List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
+        serverClasses.remove("org.slf4j.");
+        webappContext.setServerClasses(serverClasses.toArray(new String[0]));
+        webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML);
+
+        // get the temp directory for this webapp
+        File tempDir = new File(props.getWebWorkingDirectory(), warFile.getName());
+        if (tempDir.exists() && !tempDir.isDirectory()) {
+            throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory");
+        } else if (!tempDir.exists()) {
+            final boolean made = tempDir.mkdirs();
+            if (!made) {
+                throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!(tempDir.canRead() && tempDir.canWrite())) {
+            throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege");
+        }
+
+        // configure the temp dir
+        webappContext.setTempDirectory(tempDir);
+
+        // configure the max form size (3x the default)
+        webappContext.setMaxFormContentSize(600000);
+
+        // add HTTP security headers to all responses
+        final String ALL_PATHS = "/*";
+        ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class));
+        if(props.isHTTPSConfigured()) {
+            filters.add(StrictTransportSecurityFilter.class);
+        }
+        filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext));
+
+        try {
+            // configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
+            webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
+        } catch (final IOException ioe) {
+            startUpFailure(ioe);
+        }
+
+        logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
+        return webappContext;
+    }
+
+    private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) {
+        FilterHolder holder = new FilterHolder(clazz);
+        holder.setName(clazz.getSimpleName());
+        webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
+    }
+
+    private void addDocsServlets(WebAppContext docsContext) {
+        try {
+            // Load the nifi/docs directory
+            final File docsDir = getDocsDir("docs");
+
+            // load the component documentation working directory
+            final File componentDocsDirPath = props.getComponentDocumentationWorkingDirectory();
+            final File workingDocsDirectory = getWorkingDocsDirectory(componentDocsDirPath);
+
+            // Load the API docs
+            final File webApiDocsDir = getWebApiDocsDir();
+
+            // Create the servlet which will serve the static resources
+            ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class);
+            defaultHolder.setInitParameter("dirAllowed", "false");
+
+            ServletHolder docs = new ServletHolder("docs", DefaultServlet.class);
+            docs.setInitParameter("resourceBase", docsDir.getPath());
+
+            ServletHolder components = new ServletHolder("components", DefaultServlet.class);
+            components.setInitParameter("resourceBase", workingDocsDirectory.getPath());
+
+            ServletHolder restApi = new ServletHolder("rest-api", DefaultServlet.class);
+            restApi.setInitParameter("resourceBase", webApiDocsDir.getPath());
+
+            docsContext.addServlet(docs, "/html/*");
+            docsContext.addServlet(components, "/components/*");
+            docsContext.addServlet(restApi, "/rest-api/*");
+
+            docsContext.addServlet(defaultHolder, "/");
+
+            logger.info("Loading documents web app with context path set to " + docsContext.getContextPath());
+
+        } catch (Exception ex) {
+            logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage());
+            startUpFailure(ex);
+        }
+    }
+
+
+    /**
+     * Returns a File object for the directory containing NIFI documentation.
+     * <p>
+     * Formerly, if the docsDirectory did not exist NIFI would fail to start
+     * with an IllegalStateException and a rather unhelpful log message.
+     * NIFI-2184 updates the process such that if the docsDirectory does not
+     * exist an attempt will be made to create the directory. If that is
+     * successful NIFI will no longer fail and will start successfully barring
+     * any other errors. The side effect of the docsDirectory not being present
+     * is that the documentation links under the 'General' portion of the help
+     * page will not be accessible, but at least the process will be running.
+     *
+     * @param docsDirectory Name of documentation directory in installation directory.
+     * @return A File object to the documentation directory; else startUpFailure called.
+     */
+    private File getDocsDir(final String docsDirectory) {
+        File docsDir;
+        try {
+            docsDir = Paths.get(docsDirectory).toRealPath().toFile();
+        } catch (IOException ex) {
+            logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable.");
+            docsDir = new File(docsDirectory).getAbsoluteFile();
+            final boolean made = docsDir.mkdirs();
+            if (!made) {
+                logger.error("Failed to create 'docs' directory!");
+                startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created"));
+            }
+        }
+        return docsDir;
+    }
+
+    private File getWorkingDocsDirectory(final File componentDocsDirPath) {
+        File workingDocsDirectory = null;
+        try {
+            workingDocsDirectory = componentDocsDirPath.toPath().toRealPath().getParent().toFile();
+        } catch (IOException ex) {
+            logger.error("Failed to load :" + componentDocsDirPath.getAbsolutePath());
+            startUpFailure(ex);
+        }
+        return workingDocsDirectory;
+    }
+
+    private File getWebApiDocsDir() {
+        // load the rest documentation
+        final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
+        if (!webApiDocsDir.exists()) {
+            final boolean made = webApiDocsDir.mkdirs();
+            if (!made) {
+                logger.error("Failed to create " + webApiDocsDir.getAbsolutePath());
+                startUpFailure(new IOException(webApiDocsDir.getAbsolutePath() + " could not be created"));
+            }
+        }
+        return webApiDocsDir;
+    }
+
+    private void configureConnectors(final Server server) throws ServerConfigurationException {
+        // create the http configuration
+        final HttpConfiguration httpConfiguration = new HttpConfiguration();
+        final int headerSize = DataUnit.parseDataSize(props.getWebMaxHeaderSize(), DataUnit.B).intValue();
+        httpConfiguration.setRequestHeaderSize(headerSize);
+        httpConfiguration.setResponseHeaderSize(headerSize);
+
+        // Check if both HTTP and HTTPS connectors are configured and fail if both are configured
+        if (bothHttpAndHttpsConnectorsConfigured(props)) {
+            logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " +
+                    "Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty");
+            startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time"));
+        }
+
+        if (props.getSslPort() != null) {
+            configureHttpsConnector(server, httpConfiguration);
+        } else if (props.getPort() != null) {
+            configureHttpConnector(server, httpConfiguration);
+        } else {
+            logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties");
+            startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector"));
+        }
+    }
+
+    /**
+     * Configures an HTTPS connector and adds it to the server.
+     *
+     * @param server            the Jetty server instance
+     * @param httpConfiguration the configuration object for the HTTPS protocol settings
+     */
+    private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) {
+        String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
+        final Integer port = props.getSslPort();
+        String connectorLabel = "HTTPS";
+        final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces();
+        ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port);
+
+        configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc);
+    }
+
+    /**
+     * Configures an HTTP connector and adds it to the server.
+     *
+     * @param server            the Jetty server instance
+     * @param httpConfiguration the configuration object for the HTTP protocol settings
+     */
+    private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) {
+        String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
+        final Integer port = props.getPort();
+        String connectorLabel = "HTTP";
+        final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces();
+        ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c));
+
+        configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc);
+    }
+
+    /**
+     * Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar.
+     * Here the common behavior has been extracted into a shared method and the respective calling methods obtain the right values and a lambda function for the differing behavior.
+     *
+     * @param server                 the Jetty server instance
+     * @param configuration          the HTTP/HTTPS configuration instance
+     * @param hostname               the hostname from the nifi.properties file
+     * @param port                   the port to expose
+     * @param connectorLabel         used for log output (e.g. "HTTP" or "HTTPS")
+     * @param networkInterfaces      the map of network interfaces from nifi.properties
+     * @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector}
+     */
+    private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map<String, String> networkInterfaces,
+                                           ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> serverConnectorCreator) {
+        if (port < 0 || (int) Math.pow(2, 16) <= port) {
+            throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port);
+        }
+
+        logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port);
+
+        final List<Connector> serverConnectors = Lists.newArrayList();
+
+        // Calculate Idle Timeout as twice the auto-refresh interval. This ensures that even with some variance in timing,
+        // we are able to avoid closing connections from users' browsers most of the time. This can make a significant difference
+        // in HTTPS connections, as each HTTPS connection that is established must perform the SSL handshake.
+        final String autoRefreshInterval = props.getAutoRefreshInterval();
+        final long autoRefreshMillis = autoRefreshInterval == null ? 30000L : FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS);
+        final long idleTimeout = autoRefreshMillis * 2;
+
+        // If the interfaces collection is empty or each element is empty
+        if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
+            final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
+
+            // Set host and port
+            if (StringUtils.isNotBlank(hostname)) {
+                serverConnector.setHost(hostname);
+            }
+            serverConnector.setPort(port);
+            serverConnector.setIdleTimeout(idleTimeout);
+            serverConnectors.add(serverConnector);
+        } else {
+            // Add connectors for all IPs from network interfaces
+            serverConnectors.addAll(Lists.newArrayList(networkInterfaces.values().stream().map(ifaceName -> {
+                NetworkInterface iface = null;
+                try {
+                    iface = NetworkInterface.getByName(ifaceName);
+                } catch (SocketException e) {
+                    logger.error("Unable to get network interface by name {}", ifaceName, e);
+                }
+                if (iface == null) {
+                    logger.warn("Unable to find network interface named {}", ifaceName);
+                }
+                return iface;
+            }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
+                    .map(inetAddress -> {
+                        final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
+
+                        // Set host and port
+                        serverConnector.setHost(inetAddress.getHostAddress());
+                        serverConnector.setPort(port);
+                        serverConnector.setIdleTimeout(idleTimeout);
+
+                        return serverConnector;
+                    }).collect(Collectors.toList())));
+        }
+        // Add all connectors
+        serverConnectors.forEach(server::addConnector);
+    }
+
+    /**
+     * Returns true if there are configured properties for both HTTP and HTTPS connectors (specifically port because the hostname can be left blank in the HTTP connector).
+     * Prints a warning log message with the relevant properties.
+     *
+     * @param props the NiFiProperties
+     * @return true if both ports are present
+     */
+    static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) {
+        Integer httpPort = props.getPort();
+        String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
+
+        Integer httpsPort = props.getSslPort();
+        String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
+
+        if (httpPort != null && httpsPort != null) {
+            logger.warn("Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details");
+            logger.warn("HTTP connector:   http://" + httpHostname + ":" + httpPort);
+            logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort);
+            return true;
+        }
+
+        return false;
+    }
+
+    private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) {
+        // add some secure config
+        final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
+        httpsConfiguration.setSecureScheme("https");
+        httpsConfiguration.setSecurePort(port);
+        httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
+
+        // build the connector
+        return new ServerConnector(server,
+                new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
+                new HttpConnectionFactory(httpsConfiguration));
+    }
+
+    private SslContextFactory createSslContextFactory() {
+        final SslContextFactory contextFactory = new SslContextFactory();
+        configureSslContextFactory(contextFactory, props);
+        return contextFactory;
+    }
+
+    protected static void configureSslContextFactory(SslContextFactory contextFactory, NiFiProperties props) {
+        // require client auth when not supporting login, Kerberos service, or anonymous access
+        if (props.isClientAuthRequiredForRestApi()) {
+            contextFactory.setNeedClientAuth(true);
+        } else {
+            contextFactory.setWantClientAuth(true);
+        }
+
+        /* below code sets JSSE system properties when values are provided */
+        // keystore properties
+        if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_KEYSTORE))) {
+            contextFactory.setKeyStorePath(props.getProperty(NiFiProperties.SECURITY_KEYSTORE));
+        }
+        String keyStoreType = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE);
+        if (StringUtils.isNotBlank(keyStoreType)) {
+            contextFactory.setKeyStoreType(keyStoreType);
+            String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(keyStoreType);
+            if (StringUtils.isNoneEmpty(keyStoreProvider)) {
+                contextFactory.setKeyStoreProvider(keyStoreProvider);
+            }
+        }
+        final String keystorePassword = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD);
+        final String keyPassword = props.getProperty(NiFiProperties.SECURITY_KEY_PASSWD);
+        if (StringUtils.isNotBlank(keystorePassword)) {
+            // if no key password was provided, then assume the keystore password is the same as the key password.
+            final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
+            contextFactory.setKeyStorePassword(keystorePassword);
+            contextFactory.setKeyManagerPassword(defaultKeyPassword);
+        } else if (StringUtils.isNotBlank(keyPassword)) {
+            // since no keystore password was provided, there will be no keystore integrity check
+            contextFactory.setKeyManagerPassword(keyPassword);
+        }
+
+        // truststore properties
+        if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))) {
+            contextFactory.setTrustStorePath(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE));
+        }
+        String trustStoreType = props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE);
+        if (StringUtils.isNotBlank(trustStoreType)) {
+            contextFactory.setTrustStoreType(trustStoreType);
+            String trustStoreProvider = KeyStoreUtils.getKeyStoreProvider(trustStoreType);
+            if (StringUtils.isNoneEmpty(trustStoreProvider)) {
+                contextFactory.setTrustStoreProvider(trustStoreProvider);
+            }
+        }
+        if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))) {
+            contextFactory.setTrustStorePassword(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD));
+        }
+    }
+
+    @Override
+    public void start() {
+        try {
+            // Create a standard extension manager and discover extensions
+            final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager();
+            extensionManager.discoverExtensions(systemBundle, bundles);
+            extensionManager.logClassLoaderMapping();
+
+            // Set the extension manager into the holder which makes it available to the Spring context via a factory bean
+            ExtensionManagerHolder.init(extensionManager);
+
+            // Generate docs for extensions
+            DocGenerator.generate(props, extensionManager, extensionMapping);
+
+            // start the server
+            server.start();
+
+            // ensure everything started successfully
+            for (Handler handler : server.getChildHandlers()) {
+                // see if the handler is a web app
+                if (handler instanceof WebAppContext) {
+                    WebAppContext context = (WebAppContext) handler;
+
+                    // see if this webapp had any exceptions that would
+                    // cause it to be unavailable
+                    if (context.getUnavailableException() != null) {
+                        startUpFailure(context.getUnavailableException());
+                    }
+                }
+            }
+
+            // ensure the appropriate wars deployed successfully before injecting the NiFi context and security filters
+            // this must be done after starting the server (and ensuring there were no start up failures)
+            if (webApiContext != null) {
+                // give the web api the component ui extensions
+                final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
+                webApiServletContext.setAttribute("nifi-ui-extensions", componentUiExtensions);
+
+                // get the application context
+                final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
+                final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
+                final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
+
+                // component ui extensions
+                performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
+
+                // content viewer extensions
+                performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
+
+                // content viewer controller
+                if (webContentViewerContext != null) {
+                    final ContentAccess contentAccess = webApplicationContext.getBean("contentAccess", ContentAccess.class);
+
+                    // add the content access
+                    final ServletContext webContentViewerServletContext = webContentViewerContext.getServletHandler().getServletContext();
+                    webContentViewerServletContext.setAttribute("nifi-content-access", contentAccess);
+
+                    if (securityFilter != null) {
+                        webContentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
+                    }
+                }
+            }
+
+            // ensure the web document war was loaded and provide the extension mapping
+            if (webDocsContext != null) {
+                final ServletContext webDocsServletContext = webDocsContext.getServletHandler().getServletContext();
+                webDocsServletContext.setAttribute("nifi-extension-mapping", extensionMapping);
+            }
+
+            // if this nifi is a node in a cluster, start the flow service and load the flow - the
+            // flow service is loaded here for clustered nodes because the loading of the flow will
+            // initialize the connection between the node and the NCM. if the node connects (starts
+            // heartbeating, etc), the NCM may issue web requests before the application (wars) have
+            // finished loading. this results in the node being disconnected since its unable to
+            // successfully respond to the requests. to resolve this, flow loading was moved to here
+            // (after the wars have been successfully deployed) when this nifi instance is a node
+            // in a cluster
+            if (props.isNode()) {
+
+                FlowService flowService = null;
+                try {
+
+                    logger.info("Loading Flow...");
+
+                    ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(webApiContext.getServletContext());
+                    flowService = ctx.getBean("flowService", FlowService.class);
+
+                    // start and load the flow
+                    flowService.start();
+                    flowService.load(null);
+
+                    logger.info("Flow loaded successfully.");
+
+                } catch (BeansException | LifeCycleStartException | IOException | FlowSerializationException | FlowSynchronizationException | UninheritableFlowException e) {
+                    // ensure the flow service is terminated
+                    if (flowService != null && flowService.isRunning()) {
+                        flowService.stop(false);
+                    }
+                    logger.error("Unable to load flow due to: " + e, e);
+                    throw new Exception("Unable to load flow due to: " + e); // cannot wrap the exception as they are not defined in a classloader accessible to the caller
+                }
+            }
+
+            final NarLoader narLoader = new StandardNarLoader(
+                    props.getExtensionsWorkingDirectory(),
+                    props.getComponentDocumentationWorkingDirectory(),
+                    NarClassLoadersHolder.getInstance(),
+                    extensionManager,
+                    extensionMapping,
+                    this);
+
+            narAutoLoader = new NarAutoLoader(props.getNarAutoLoadDirectory(), narLoader);
+            narAutoLoader.start();
+
+            URI jarsIndex = props.getDCAEJarIndexURI();
+
+            // REVIEW: Added ability to turn off the loaidng of dcae jars by providing no url
+            if (jarsIndex == null) {
+                StringBuilder sb = new StringBuilder();
+                sb.append("Auto-loading of DCAE jars is turned off.");
+                sb.append(" You must set the value of \"nifi.dcae.jars.index.url\"");
+                sb.append(" to the full url to the index JSON of DCAE jars in the nifi.properties file");
+                sb.append(" in order to activate this feature.");
+                logger.warn(sb.toString());
+            } else {
+                this.dcaeAutoLoader = new DCAEAutoLoader();
+                this.dcaeAutoLoader.start(jarsIndex, extensionManager);
+            }
+
+            // dump the application url after confirming everything started successfully
+            dumpUrls();
+        } catch (Exception ex) {
+            startUpFailure(ex);
+        }
+    }
+
+    private void performInjectionForComponentUis(final Collection<WebAppContext> componentUiExtensionWebContexts,
+                                                 final NiFiWebConfigurationContext configurationContext, final FilterHolder securityFilter) {
+        if (CollectionUtils.isNotEmpty(componentUiExtensionWebContexts)) {
+            for (final WebAppContext customUiContext : componentUiExtensionWebContexts) {
+                // set the NiFi context in each custom ui servlet context
+                final ServletContext customUiServletContext = customUiContext.getServletHandler().getServletContext();
+                customUiServletContext.setAttribute("nifi-web-configuration-context", configurationContext);
+
+                // add the security filter to any ui extensions wars
+                if (securityFilter != null) {
+                    customUiContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
+                }
+            }
+        }
+    }
+
+    private void performInjectionForContentViewerUis(final Collection<WebAppContext> contentViewerWebContexts,
+                                                     final FilterHolder securityFilter) {
+        if (CollectionUtils.isNotEmpty(contentViewerWebContexts)) {
+            for (final WebAppContext contentViewerContext : contentViewerWebContexts) {
+                // add the security filter to any content viewer  wars
+                if (securityFilter != null) {
+                    contentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
+                }
+            }
+        }
+    }
+
+    private void dumpUrls() throws SocketException {
+        final List<String> urls = new ArrayList<>();
+
+        for (Connector connector : server.getConnectors()) {
+            if (connector instanceof ServerConnector) {
+                final ServerConnector serverConnector = (ServerConnector) connector;
+
+                Set<String> hosts = new HashSet<>();
+
+                // determine the hosts
+                if (StringUtils.isNotBlank(serverConnector.getHost())) {
+                    hosts.add(serverConnector.getHost());
+                } else {
+                    Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
+                    if (networkInterfaces != null) {
+                        for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
+                            for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
+                                hosts.add(inetAddress.getHostAddress());
+                            }
+                        }
+                    }
+                }
+
+                // ensure some hosts were found
+                if (!hosts.isEmpty()) {
+                    String scheme = "http";
+                    if (props.getSslPort() != null && serverConnector.getPort() == props.getSslPort()) {
+                        scheme = "https";
+                    }
+
+                    // dump each url
+                    for (String host : hosts) {
+                        urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort()));
+                    }
+                }
+            }
+        }
+
+        if (urls.isEmpty()) {
+            logger.warn("NiFi has started, but the UI is not available on any hosts. Please verify the host properties.");
+        } else {
+            // log the ui location
+            logger.info("NiFi has started. The UI is available at the following URLs:");
+            for (final String url : urls) {
+                logger.info(String.format("%s/nifi", url));
+            }
+        }
+    }
+
+    private void startUpFailure(Throwable t) {
+        System.err.println("Failed to start web server: " + t.getMessage());
+        System.err.println("Shutting down...");
+        logger.warn("Failed to start web server... shutting down.", t);
+        System.exit(1);
+    }
+
+    @Override
+    public void setExtensionMapping(ExtensionMapping extensionMapping) {
+        this.extensionMapping = extensionMapping;
+    }
+
+    @Override
+    public void setBundles(Bundle systemBundle, Set<Bundle> bundles) {
+        this.systemBundle = systemBundle;
+        this.bundles = bundles;
+    }
+
+    @Override
+    public void stop() {
+        try {
+            server.stop();
+        } catch (Exception ex) {
+            logger.warn("Failed to stop web server", ex);
+        }
+
+        try {
+            if (narAutoLoader != null) {
+                narAutoLoader.stop();
+            }
+
+            if (dcaeAutoLoader != null) {
+                dcaeAutoLoader.stop();
+            }
+        } catch (Exception e) {
+            logger.warn("Failed to stop NAR auto-loader", e);
+        }
+    }
+
+    /**
+     * Holds the result of loading WARs for custom UIs.
+     */
+    private static class ExtensionUiInfo {
+
+        private final Collection<WebAppContext> webAppContexts;
+        private final Map<String, String> mimeMappings;
+        private final Collection<WebAppContext> componentUiExtensionWebContexts;
+        private final Collection<WebAppContext> contentViewerWebContexts;
+        private final Map<String, List<UiExtension>> componentUiExtensionsByType;
+
+        public ExtensionUiInfo(final Collection<WebAppContext> webAppContexts,
+                               final Map<String, String> mimeMappings,
+                               final Collection<WebAppContext> componentUiExtensionWebContexts,
+                               final Collection<WebAppContext> contentViewerWebContexts,
+                               final Map<String, List<UiExtension>> componentUiExtensionsByType) {
+            this.webAppContexts = webAppContexts;
+            this.mimeMappings = mimeMappings;
+            this.componentUiExtensionWebContexts = componentUiExtensionWebContexts;
+            this.contentViewerWebContexts = contentViewerWebContexts;
+            this.componentUiExtensionsByType = componentUiExtensionsByType;
+        }
+
+        public Collection<WebAppContext> getWebAppContexts() {
+            return webAppContexts;
+        }
+
+        public Map<String, String> getMimeMappings() {
+            return mimeMappings;
+        }
+
+        public Collection<WebAppContext> getComponentUiExtensionWebContexts() {
+            return componentUiExtensionWebContexts;
+        }
+
+        public Collection<WebAppContext> getContentViewerWebContexts() {
+            return contentViewerWebContexts;
+        }
+
+        public Map<String, List<UiExtension>> getComponentUiExtensionsByType() {
+            return componentUiExtensionsByType;
+        }
+    }
+}
+
+@FunctionalInterface
+interface ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> {
+    ServerConnector create(Server server, HttpConfiguration httpConfiguration);
+}
diff --git a/mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties b/mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties
new file mode 100644 (file)
index 0000000..1943157
--- /dev/null
@@ -0,0 +1,19 @@
+# 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.
+
+nf.canvas.script.tags=<script type="text/javascript" src="js/nf/canvas/nf-canvas-all.js?${project.version}"></script>
+nf.canvas.style.tags=<link rel="stylesheet" href="css/nf-canvas-all.css?${project.version}" type="text/css" />\n\
+<link rel="stylesheet" href="css/message-pane.css?${project.version}" type="text/css" />\n\
+<link rel="stylesheet" href="css/nf-common-ui.css?${project.version}" type="text/css" />
\ No newline at end of file
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp
new file mode 100644 (file)
index 0000000..ccfee0c
--- /dev/null
@@ -0,0 +1,167 @@
+<%--
+ 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.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>NiFi</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+        <link rel="shortcut icon" href="images/nifi16.ico"/>
+        <link rel="stylesheet" href="assets/reset.css/reset.css" type="text/css" />
+        ${nf.canvas.style.tags}
+        <link rel="stylesheet" href="js/codemirror/lib/codemirror.css" type="text/css" />
+        <link rel="stylesheet" href="js/codemirror/addon/hint/show-hint.css" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/nfeditor/jquery.nfeditor.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/nfeditor/languages/nfel.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/tabbs/jquery.tabbs.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/combo/jquery.combo.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/propertytable/jquery.propertytable.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/tagcloud/jquery.tagcloud.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="js/jquery/modal/jquery.modal.css?${project.version}" type="text/css" />
+        <link rel="stylesheet" href="assets/qtip2/dist/jquery.qtip.min.css?" type="text/css" />
+        <link rel="stylesheet" href="assets/jquery-ui-dist/jquery-ui.min.css" type="text/css" />
+        <link rel="stylesheet" href="assets/jquery-minicolors/jquery.minicolors.css" type="text/css" />
+        <link rel="stylesheet" href="assets/slickgrid/slick.grid.css" type="text/css" />
+        <link rel="stylesheet" href="css/slick-nifi-theme.css" type="text/css" />
+        <link rel="stylesheet" href="fonts/flowfont/flowfont.css" type="text/css" />
+        <link rel="stylesheet" href="assets/angular-material/angular-material.min.css" type="text/css" />
+        <link rel="stylesheet" href="assets/font-awesome/css/font-awesome.min.css" type="text/css" />
+        <script>
+            //force browsers to use URLSearchParams polyfill do to bugs and inconsistent browser implementations
+            URLSearchParams = undefined;
+        </script>
+        <script type="text/javascript" src="assets/url-search-params/build/url-search-params.js"></script>
+        <script type="text/javascript" src="js/codemirror/lib/codemirror-compressed.js"></script>
+        <script type="text/javascript" src="assets/d3/build/d3.min.js"></script>
+        <script type="text/javascript" src="assets/d3-selection-multi/build/d3-selection-multi.min.js"></script>
+        <script type="text/javascript" src="assets/jquery/dist/jquery.min.js"></script>
+        <script type="text/javascript" src="assets/jquery-ui-dist/jquery-ui.min.js"></script>
+        <script type="text/javascript" src="js/jquery/jquery.base64.js"></script>
+        <script type="text/javascript" src="js/jquery/jquery.center.js"></script>
+        <script type="text/javascript" src="js/jquery/jquery.ellipsis.js"></script>
+        <script type="text/javascript" src="js/jquery/jquery.each.js"></script>
+        <script type="text/javascript" src="js/jquery/jquery.tab.js"></script>
+        <script type="text/javascript" src="assets/jquery-form/jquery.form.js"></script>
+        <script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script>
+        <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script>
+        <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script>
+        <script type="text/javascript" src="assets/jquery-minicolors/jquery.minicolors.min.js"></script>
+        <script type="text/javascript" src="assets/qtip2/dist/jquery.qtip.min.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/plugins/slick.cellselectionmodel.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/plugins/slick.rowselectionmodel.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/plugins/slick.autotooltips.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/slick.formatters.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/slick.editors.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/slick.dataview.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/slick.core.js"></script>
+        <script type="text/javascript" src="assets/slickgrid/slick.grid.js"></script>
+        <script type="text/javascript" src="assets/angular/angular.min.js"></script>
+        <script type="text/javascript" src="assets/angular-messages/angular-messages.min.js"></script>
+        <script type="text/javascript" src="assets/angular-resource/angular-resource.min.js"></script>
+        <script type="text/javascript" src="assets/angular-route/angular-route.min.js"></script>
+        <script type="text/javascript" src="assets/angular-aria/angular-aria.min.js"></script>
+        <script type="text/javascript" src="assets/angular-animate/angular-animate.min.js"></script>
+        <script type="text/javascript" src="assets/angular-material/angular-material.min.js"></script>
+        <script type="text/javascript" src="assets/JSON2/json2.js"></script>
+        <script type="text/javascript" src="js/nf/nf-namespace.js?${project.version}"></script>
+        <script type="text/javascript" src="js/nf/nf-ng-namespace.js?${project.version}"></script>
+        <script type="text/javascript" src="js/nf/canvas/nf-ng-canvas-namespace.js?${project.version}"></script>
+        ${nf.canvas.script.tags}
+        <script type="text/javascript" src="js/jquery/nfeditor/languages/nfel.js?${project.version}"></script>
+        <script type="text/javascript" src="js/jquery/nfeditor/jquery.nfeditor.js?${project.version}"></script>
+        <script type="text/javascript" src="js/jquery/propertytable/jquery.propertytable.js?${project.version}"></script>
+        <script type="text/javascript" src="js/jquery/tagcloud/jquery.tagcloud.js?${project.version}"></script>
+
+        <script type="text/javascript" src="js/jquery/dcae-mod.js"></script>
+
+    </head>
+    <body ng-controller="ngCanvasAppCtrl" id="canvas-body">
+        <div id="splash">
+            <div id="splash-img" layout="row" layout-align="center center">
+                <md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="150"></md-progress-circular>
+            </div>
+        </div>
+        <jsp:include page="/WEB-INF/partials/message-pane.jsp"/>
+        <jsp:include page="/WEB-INF/partials/banners-main.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/canvas-header.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/flow-status.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/about-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/ok-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/yes-no-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/status-history-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/search-users-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/disable-controller-service-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/enable-controller-service-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-controller-service-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-reporting-task-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-processor-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-port-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-process-group-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/new-template-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/upload-template-dialog.jsp"/>
+        <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/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"/>
+        <jsp:include page="/WEB-INF/partials/canvas/distribution-environment-dialog.jsp"/>
+        <div id="canvas-container" class="unselectable"></div>
+        <div id="canvas-tooltips">
+            <div id="processor-tooltips"></div>
+            <div id="port-tooltips"></div>
+            <div id="process-group-tooltips"></div>
+            <div id="remote-process-group-tooltips"></div>
+        </div>
+        <jsp:include page="/WEB-INF/partials/canvas/navigation.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/settings-content.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/shell.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/controller-service-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/reporting-task-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/processor-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/processor-details.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/variable-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/process-group-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/override-policy-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/policy-management.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/remote-process-group-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/remote-process-group-details.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/remote-process-group-ports.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/remote-port-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/port-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/port-details.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/label-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/connection-configuration.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/drop-request-status-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/flowfile-details-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/listing-request-status-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/queue-listing.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/component-state-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/component-version-dialog.jsp"/>
+        <jsp:include page="/WEB-INF/partials/connection-details.jsp"/>
+        <div id="context-menu" class="context-menu unselectable"></div>
+        <span id="nifi-content-viewer-url" class="hidden"></span>
+    </body>
+</html>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp
new file mode 100644 (file)
index 0000000..3afbe66
--- /dev/null
@@ -0,0 +1,191 @@
+<%--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<md-toolbar id="header" layout-align="space-between center" layout="row" class="md-small md-accent md-hue-1">
+    <img id="nifi-logo" src="images/dcae-logo.png">
+    <div flex layout="row" layout-align="space-between center">
+        <div id="component-container">
+            <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.processor}}"
+                    id="processor-component"
+                    class="component-button icon icon-processor"
+                    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.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.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.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.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.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.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.canWriteCurrentGroup();"
+                               nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.labelComponent);">
+                           <span class="component-button-grip"></span>
+                       </button>
+        </div>
+        <div layout="row" layout-align="space-between center">
+            <div layout-align="space-between end" layout="column">
+                <div layout="row" layout-align="space-between center" id="current-user-container">
+                    <span id="anonymous-user-alert" class="hidden fa fa-warning"></span>
+                    <div></div>
+                    <div id="current-user"></div>
+                </div>
+                <div id="login-link-container">
+                    <span id="login-link" class="link"
+                          ng-click="appCtrl.serviceProvider.headerCtrl.loginCtrl.shell.launch();">log in</span>
+                </div>
+                <div id="logout-link-container" style="display: none;">
+                    <span id="logout-link" class="link"
+                          ng-click="appCtrl.serviceProvider.headerCtrl.logoutCtrl.logout();">log out</span>
+                </div>
+            </div>
+            <md-menu md-position-mode="target-right target" md-offset="-1 44">
+                <button md-menu-origin id="global-menu-button" ng-click="$mdMenu.open()">
+                    <div class="fa fa-navicon"></div>
+                </button>
+                <md-menu-content id="global-menu-content">
+                    <md-menu-item layout-align="space-around center">
+                        <a id="reporting-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.summary.shell.launch();">
+                            <i class="fa fa-table"></i>Summary
+                        </a>
+                    </md-menu-item>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="counters-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.counters.shell.launch();"
+                           ng-class="{disabled: !appCtrl.nf.Common.canAccessCounters()}">
+                            <i class="icon icon-counter"></i>Counters
+                        </a>
+                    </md-menu-item>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="bulletin-board-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.bulletinBoard.shell.launch();">
+                            <i class="fa fa-sticky-note-o"></i>Bulletin Board
+                        </a>
+                    </md-menu-item>
+                    <md-menu-divider></md-menu-divider>
+                    <md-menu-item
+                            layout-align="space-around center">
+                        <a id="provenance-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.dataProvenance.shell.launch();"
+                           ng-class="{disabled: !appCtrl.nf.Common.canAccessProvenance()}">
+                            <i class="icon icon-provenance"></i>Data Provenance
+                        </a>
+                    </md-menu-item>
+                    <md-menu-divider></md-menu-divider>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="flow-settings-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.controllerSettings.shell.launch();">
+                            <i class="fa fa-wrench"></i>Controller Settings
+                        </a>
+                    </md-menu-item>
+                    <md-menu-item ng-if="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.cluster.visible();"
+                                  layout-align="space-around center">
+                        <a id="cluster-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.cluster.shell.launch();"
+                           ng-class="{disabled: !appCtrl.nf.Common.canAccessController()}">
+                            <i class="fa fa-cubes"></i>Cluster
+                        </a>
+                    </md-menu-item>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="history-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.flowConfigHistory.shell.launch();">
+                            <i class="fa fa-history"></i>Flow Configuration History
+                        </a>
+                    </md-menu-item>
+                    <md-menu-divider ng-if="appCtrl.nf.CanvasUtils.isManagedAuthorizer()"></md-menu-divider>
+                    <md-menu-item layout-align="space-around center" ng-if="appCtrl.nf.CanvasUtils.isManagedAuthorizer()">
+                        <a id="users-link" layout="row"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.users.shell.launch();"
+                           ng-class="{disabled: !(appCtrl.nf.Common.canAccessTenants())}">
+                            <i class="fa fa-users"></i>Users
+                        </a>
+                    </md-menu-item>
+                    <md-menu-item layout-align="space-around center" ng-if="appCtrl.nf.CanvasUtils.isManagedAuthorizer()">
+                        <a id="policies-link" layout="row"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.policies.shell.launch();"
+                           ng-class="{disabled: !(appCtrl.nf.Common.canAccessTenants() && appCtrl.nf.Common.canModifyPolicies())}">
+                            <i class="fa fa-key"></i>Policies
+                        </a>
+                    </md-menu-item>
+                    <md-menu-divider></md-menu-divider>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="templates-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.templates.shell.launch();">
+                            <i class="icon icon-template"></i>Templates
+                        </a>
+                    </md-menu-item>
+                    <md-menu-divider></md-menu-divider>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="help-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.help.shell.launch();">
+                            <i class="fa fa-question-circle"></i>Help
+                        </a>
+                    </md-menu-item>
+                    <md-menu-item layout-align="space-around center">
+                        <a id="about-link"
+                           ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.about.modal.show();">
+                            <i class="fa fa-info-circle"></i>About
+                        </a>
+                    </md-menu-item>
+                </md-menu-content>
+            </md-menu>
+        </div>
+    </div>
+</md-toolbar>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp
new file mode 100644 (file)
index 0000000..c0e368c
--- /dev/null
@@ -0,0 +1,218 @@
+<%--
+ 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.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="connection-configuration" layout="column" class="hidden large-dialog">
+    <div class="connection-configuration-tab-container dialog-content">
+        <div id="connection-configuration-tabs" class="tab-container"></div>
+        <div id="connection-configuration-tabs-content">
+            <div id="connection-settings-tab-content" class="configuration-tab">
+                <div class="settings-left">
+                    <div class="setting">
+                        <div class="setting-name">Name</div>
+                        <div class="setting-field">
+                            <input type="text" id="connection-name" name="connection-name" class="setting-input"/>
+                        </div>
+                    </div>
+                    <div class="setting">
+                        <div class="setting-name">Id</div>
+                        <div class="setting-field">
+                            <span type="text" id="connection-id"></span>
+                        </div>
+                    </div>
+                    <div class="setting">
+                        <div class="setting-name">
+                            FlowFile expiration
+                            <div class="fa fa-question-circle" alt="Info" title="The maximum amount of time an object may be in the flow before it will be automatically aged out of the flow."></div>
+                        </div>
+                        <div class="setting-field">
+                            <input type="text" id="flow-file-expiration" name="flow-file-expiration" class="setting-input"/>
+                        </div>
+                    </div>
+                    <div class="multi-column-settings">
+                        <div class="setting">
+                            <div class="setting-name">
+                                Back Pressure<br/>Object threshold
+                                <div class="fa fa-question-circle" alt="Info" title="The maximum number of objects that can be queued before back pressure is applied."></div>
+                            </div>
+                            <div class="setting-field">
+                                <input type="text" id="back-pressure-object-threshold" name="back-pressure-object-threshold" class="setting-input"/>
+                            </div>
+                        </div>
+                        <div class="separator">&nbsp;</div>
+                        <div class="setting">
+                            <div class="setting-name">
+                                &nbsp;<br/>Size threshold
+                                <div class="fa fa-question-circle" alt="Info" title="The maximum data size of objects that can be queued before back pressure is applied."></div>
+                            </div>
+                            <div class="setting-field">
+                                <input type="text" id="back-pressure-data-size-threshold" name="back-pressure-data-size-threshold" class="setting-input"/>
+                            </div>
+                        </div>
+                    </div>
+                    <div>
+                        <div class="multi-column-settings">
+                            <div class="setting">
+                                <div class="setting-name">
+                                    Load Balance Strategy
+                                    <div class="fa fa-question-circle" alt="Info" title="How to load balance the data in this Connection across the nodes in the cluster."></div>
+                                </div>
+                                <div class="setting-field">
+                                    <div id="load-balance-strategy-combo"></div>
+                                </div>
+                            </div>
+                            <div id="load-balance-partition-attribute-setting-separator" class="separator">&nbsp;</div>
+                            <div id="load-balance-partition-attribute-setting" class="setting">
+                                <div class="setting-name">
+                                    Attribute Name
+                                    <div class="fa fa-question-circle" alt="Info" title="The FlowFile Attribute to use for determining which node a FlowFile will go to."></div>
+                                </div>
+                                <div class="setting-field">
+                                    <input type="text" id="load-balance-partition-attribute" name="load-balance-partition-attribute" class="setting-input"/>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="load-balance-compression-setting" class="setting">
+                            <div class="setting-name">
+                                Load Balance Compression
+                                <div class="fa fa-question-circle" alt="Info" title="Whether or not data should be compressed when being transferred between nodes in the cluster."></div>
+                            </div>
+                            <div class="setting-field">
+                                <div id="load-balance-compression-combo"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="spacer">&nbsp;</div>
+                <div class="settings-right">
+                    <div class="setting">
+                        <div class="setting-name">
+                            Available prioritizers
+                            <div class="fa fa-question-circle" alt="Info" title="Available prioritizers that could reprioritize FlowFiles in this work queue."></div>
+                        </div>
+                        <div class="setting-field">
+                            <ul id="prioritizer-available"></ul>
+                        </div>
+                    </div>
+                    <div class="setting">
+                        <div class="setting-name">
+                            Selected prioritizers
+                            <div class="fa fa-question-circle" alt="Info" title="Prioritizers that have been selected to reprioritize FlowFiles in this work queue."></div>
+                        </div>
+                        <div class="setting-field">
+                            <ul id="prioritizer-selected"></ul>
+                        </div>
+                    </div>
+                </div>
+                <input type="hidden" id="connection-uri" name="connection-uri"/>
+                <input type="hidden" id="connection-source-component-id" name="connection-source-component-id"/>
+                <input type="hidden" id="connection-source-id" name="connection-source-id"/>
+                <input type="hidden" id="connection-source-group-id" name="connection-source-group-id"/>
+                <input type="hidden" id="connection-destination-component-id" name="connection-destination-component-id"/>
+                <input type="hidden" id="connection-destination-id" name="connection-destination-id"/>
+                <input type="hidden" id="connection-destination-group-id" name="connection-destination-group-id"/>
+            </div>
+            <div id="connection-details-tab-content" class="configuration-tab">
+                <div class="settings-left">
+                    <div id="read-only-output-port-source" class="setting hidden">
+                        <div class="setting-name">From output</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="read-only-output-port-name" class="ellipsis"></div>
+                        </div>
+                    </div>
+                    <div id="output-port-source" class="setting hidden">
+                        <div class="setting-name">From output</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="output-port-options"></div>
+                        </div>
+                    </div>
+                    <div id="input-port-source" class="setting hidden">
+                        <div class="setting-name">From input</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="input-port-source-name" class="label ellipsis"></div>
+                        </div>
+                    </div>
+                    <div id="funnel-source" class="setting hidden">
+                        <div class="setting-name">From funnel</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="funnel-source-name" class="label ellipsis" title="funnel">funnel</div>
+                        </div>
+                    </div>
+                    <div id="processor-source" class="setting hidden">
+                        <div class="setting-name">From processor</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="processor-source-details">
+                                <div id="processor-source-name" class="label ellipsis"></div>
+                                <div id="processor-source-type" class="ellipsis"></div>
+                            </div>
+                        </div>
+                    </div>
+                    <div id="connection-source-group" class="setting">
+                        <div class="setting-name">Within group</div>
+                        <div class="setting-field">
+                            <div id="connection-source-group-name"></div>
+                        </div>
+                    </div>
+                </div>
+                <div class="spacer">&nbsp;</div>
+                <div class="settings-right">
+                    <div id="input-port-destination" class="setting hidden">
+                        <div class="setting-name">To input</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="input-port-options"></div>
+                        </div>
+                    </div>
+                    <div id="output-port-destination" class="setting hidden">
+                        <div class="setting-name">To output</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="output-port-destination-name" class="label ellipsis"></div>
+                        </div>
+                    </div>
+                    <div id="funnel-destination" class="setting hidden">
+                        <div class="setting-name">To funnel</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="funnel-destination-name" class="label ellipsis" title="funnel">funnel</div>
+                        </div>
+                    </div>
+                    <div id="processor-destination" class="setting hidden">
+                        <div class="setting-name">To processor</div>
+                        <div class="setting-field connection-terminal-label">
+                            <div id="processor-destination-details">
+                                <div id="processor-destination-name" class="label ellipsis"></div>
+                                <div id="processor-destination-type" class="ellipsis"></div>
+                            </div>
+                        </div>
+                    </div>
+                    <div id="connection-destination-group" class="setting">
+                        <div class="setting-name">Within group</div>
+                        <div class="setting-field">
+                            <div id="connection-destination-group-name"></div>
+                        </div>
+                    </div>
+                </div>
+                <div id="relationship-names-container" class="hidden">
+                    <div class="setting-name">Data relationships</div>
+                    <div class="setting-field">
+                        <div id="relationship-names"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp
new file mode 100644 (file)
index 0000000..3d7e8d9
--- /dev/null
@@ -0,0 +1,42 @@
+<%--
+================================================================================
+Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+================================================================================
+Licensed 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.
+============LICENSE_END=========================================================
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="distribution-environment-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="distribution-environment-id" class="hidden"></span>
+                <input type="text" id="distribution-environment-name" class="setting-input"/>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Runtime API URL</div>
+            <div class="setting-field">
+                <input type="text" id="distribution-environment-location" class="setting-input" placeholder="http://runtime-host:port"/>
+            </div>
+        </div>
+        <div class="setting">
+            <div class="setting-name">Description</div>
+            <div class="setting-field">
+                <textarea id="distribution-environment-description" class="setting-input"></textarea>
+            </div>
+        </div>
+
+    </div>
+</div>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp
new file mode 100644 (file)
index 0000000..2efe0a1
--- /dev/null
@@ -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.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="flow-status" flex layout="row" layout-align="space-between center">
+      <div id="flow-status-container" layout="row" layout-align="space-around center">
+      </div>
+
+      <div layout="row" layout-align="end center">
+          <div id="search-container">
+              <button id="search-button" ng-click="appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.search.toggleSearchField();"><i class="fa fa-search"></i></button>
+              <input id="search-field" type="text" placeholder="Search"/>
+          </div>
+          <button id="bulletin-button"><i class="fa fa-sticky-note-o"></i></button>
+      </div>
+   </div>
+<div id="search-flow-results"></div>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp
new file mode 100644 (file)
index 0000000..caf7278
--- /dev/null
@@ -0,0 +1,129 @@
+<%--
+ 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.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<nf-breadcrumbs
+        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"
+        is-tracking="appCtrl.serviceProvider.breadcrumbsCtrl.isTracking"
+        get-version-control-class="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlClass"
+        get-version-control-tooltip="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlTooltip">
+</nf-breadcrumbs>
+<div id="graph-controls">
+    <div id="navigation-control" class="graph-control">
+        <div class="graph-control-docked pointer fa fa-compass" title="Navigate"
+             ng-click="appCtrl.serviceProvider.graphControlsCtrl.undock($event)">
+        </div>
+        <div class="graph-control-header-container hidden pointer"
+             ng-click="appCtrl.serviceProvider.graphControlsCtrl.expand($event)">
+            <div class="graph-control-header-icon fa fa-compass">
+            </div>
+            <div class="graph-control-header">Navigate</div>
+            <div class="graph-control-header-action">
+                <div class="graph-control-expansion fa fa-plus-square-o pointer"></div>
+            </div>
+            <div class="clear"></div>
+        </div>
+        <div class="graph-control-content hidden">
+            <div id="navigation-buttons">
+                <div id="naviagte-zoom-in" class="action-button" title="Zoom In"
+                     ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomIn();">
+                    <button><div class="graph-control-action-icon fa fa-search-plus"></div></button>
+                </div>
+                <div class="button-spacer-small">&nbsp;</div>
+                <div id="naviagte-zoom-out" class="action-button" title="Zoom Out"
+                     ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomOut();">
+                    <button><div class="graph-control-action-icon fa fa-search-minus"></div></button>
+                </div>
+                <div class="button-spacer-large">&nbsp;</div>
+                <div id="naviagte-zoom-fit" class="action-button" title="Fit"
+                     ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomFit();">
+                    <button><div class="graph-control-action-icon icon icon-zoom-fit"></div></button>
+                </div>
+                <div class="button-spacer-small">&nbsp;</div>
+                <div id="naviagte-zoom-actual-size" class="action-button" title="Actual"
+                     ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomActualSize();">
+                    <button><div class="graph-control-action-icon icon icon-zoom-actual"></div></button>
+                </div>
+                <div class="clear"></div>
+            </div>
+            <div id="birdseye"></div>
+        </div>
+    </div>
+    <div id="operation-control" class="graph-control">
+        <div class="graph-control-docked pointer fa fa-hand-o-up" title="Operate"
+             ng-click="appCtrl.serviceProvider.graphControlsCtrl.undock($event)">
+        </div>
+        <div class="graph-control-header-container hidden pointer"
+             ng-click="appCtrl.serviceProvider.graphControlsCtrl.expand($event)">
+            <div class="graph-control-header-icon fa fa-hand-o-up">
+            </div>
+            <div class="graph-control-header">Operate</div>
+            <div class="graph-control-header-action">
+                <div class="graph-control-expansion fa fa-plus-square-o pointer"></div>
+            </div>
+            <div class="clear"></div>
+        </div>
+        <div class="graph-control-content hidden">
+            <div id="operation-context">
+                <div id="operation-context-logo">
+                    <i class="icon" ng-class="appCtrl.serviceProvider.graphControlsCtrl.getContextIcon()"></i>
+                </div>
+                <div id="operation-context-details-container">
+                    <div id="operation-context-name"><strong> {{appCtrl.serviceProvider.graphControlsCtrl.getContextName()}} </strong></div>
+                    <div id="operation-context-type" ng-class="appCtrl.serviceProvider.graphControlsCtrl.hide()">{{appCtrl.serviceProvider.graphControlsCtrl.getContextType()}}</div>
+                </div>
+                <div class="clear"></div>
+                <div id="operation-context-id" ng-class="appCtrl.serviceProvider.graphControlsCtrl.hide()">{{appCtrl.serviceProvider.graphControlsCtrl.getContextId()}}</div>
+            </div>            <div id="operation-buttons">
+                <div>
+
+                  <div id="operation-context-type">Distribute for deployment:</div>
+                  <br>
+                  <div>
+                    <select name="environment" id="environmentType" class="combo" onchange="onEnvironmentSelect()">
+                      <option class="combo-option-text" disabled selected>Select Environment</option>
+                    </select>
+                  </div>
+
+                    <br>
+                   <div class="button-spacer-large">&nbsp;</div>
+                   <div id="operate-refresh" class="action-button" title="Refresh Environments">
+                        <button id="refresh-env-btn" onclick="refreshEnvironments()" >
+                        <div class="graph-control-action-icon fa fa-refresh"></div><span></span></button>
+                    </div>
+                    <div class="button-spacer-large">&nbsp;</div>
+                    <div id="operate-delete" class="action-button" title="Delete">
+                        <button ng-click="appCtrl.nf.Actions['delete'](appCtrl.nf.CanvasUtils.getSelection());"
+                                ng-disabled="!appCtrl.nf.CanvasUtils.areDeletable(appCtrl.nf.CanvasUtils.getSelection());">
+                            <div class="graph-control-action-icon fa fa-trash"></div><span></span></button>
+                    </div>
+                     <div class="button-spacer-large">&nbsp;</div>
+                     <div id="operate-submit" class="action-button" title="Submit">
+                            <button id="operate-submit-btn" onclick="distributeGraph()" >
+                             <div class="graph-control-action-icon fa fa-check"></div><span></span></button>
+                      </div>
+                      <div class="clear"></div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp
new file mode 100644 (file)
index 0000000..b540ff7
--- /dev/null
@@ -0,0 +1,86 @@
+<%--
+ 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.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="settings" class="hidden">
+    <div id="settings-header-text" class="settings-header-text">NiFi Settings</div>
+    <div class="settings-container">
+        <div>
+            <div id="settings-tabs" class="settings-tabs tab-container"></div>
+            <div class="clear"></div>
+        </div>
+        <div id="settings-tabs-content">
+            <button id="new-service-or-task" class="add-button fa fa-plus" title="Create a new reporting task controller service" style="display: block;"></button>
+            <div id="general-settings-tab-content" class="configuration-tab">
+                <div id="general-settings">
+                    <div class="setting">
+                        <div class="setting-name">
+                            Maximum timer driven thread count
+                            <div class="fa fa-question-circle" alt="Info" title="The maximum number of threads for timer driven processors available to the system."></div>
+                        </div>
+                        <div class="editable setting-field">
+                            <input type="text" id="maximum-timer-driven-thread-count-field" class="setting-input"/>
+                        </div>
+                        <div class="read-only setting-field">
+                            <span id="read-only-maximum-timer-driven-thread-count-field"></span>
+                        </div>
+                    </div>
+                    <div class="setting">
+                        <div class="setting-name">
+                            Maximum event driven thread count
+                            <div class="fa fa-question-circle" alt="Info" title="The maximum number of threads for event driven processors available to the system."></div>
+                        </div>
+                        <div class="editable setting-field">
+                            <input type="text" id="maximum-event-driven-thread-count-field" class="setting-input"/>
+                        </div>
+                        <div class="read-only setting-field">
+                            <span id="read-only-maximum-event-driven-thread-count-field"></span>
+                        </div>
+                    </div>
+                    <div class="editable settings-buttons">
+                        <div id="settings-save" class="button">Apply</div>
+                        <div class="clear"></div>
+                    </div>
+                </div>
+            </div>
+            <div id="controller-services-tab-content" class="configuration-tab controller-settings-table">
+                <div id="controller-services-table" class="settings-table"></div>
+            </div>
+            <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 id="distribution-environment-content" class="configuration-tab controller-settings-table">
+                <div id="distribution-environments-table" class="settings-table" style="width:100%; margin:20px;"></div>
+             </div>
+
+        </div>
+    </div>
+    <div id="settings-refresh-container">
+        <button id="settings-refresh-button" class="refresh-button pointer fa fa-refresh" title="Refresh"></button>
+        <div id="settings-last-refreshed-container" class="last-refreshed-container">
+            Last updated:&nbsp;<span id="settings-last-refreshed" class="value-color"></span>
+        </div>
+        <div id="settings-loading-container" class="loading-container"></div>
+        <div id="controller-cs-availability" class="hidden">Listed services are available to all Reporting Tasks and services defined in the Controller Settings.</div>
+        <div class="clear"></div>
+    </div>
+</div>
diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp
new file mode 100644 (file)
index 0000000..5b6c4c3
--- /dev/null
@@ -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.
+
+  Modifications to the original nifi code for the ONAP project are made
+  available under the Apache License, Version 2.0
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="shell-dialog" class="hidden cancellable">
+    <div id="shell-container" class="dialog-content">
+        <div id="shell-close-container">
+            <button id="shell-undock-button" class="undock-normal pointer " title="Open in New Window">
+                <div class="fa fa-external-link-square"></div>
+            </button>
+            <button id="shell-close-button" class="close-normal pointer" onclick="onCloseSettings()" title="Close">
+                <div class="fa fa-times"></div>
+            </button>
+            <div class="clear"></div>
+        </div>
+        <div id="shell"></div>
+    </div>
+</div>
diff --git a/mod/designtool/designtool-web/src/main/webapp/css/navigation.css b/mod/designtool/designtool-web/src/main/webapp/css/navigation.css
new file mode 100644 (file)
index 0000000..05adc45
--- /dev/null
@@ -0,0 +1,338 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* general graph control styles */
+
+#graph-controls {
+    position: absolute;
+    left: 0;
+    top: 110px;
+    z-index: 2;
+}
+
+#graph-controls .icon {
+    font-size: 18px;
+    line-height: 23px;
+    margin-left: -2px;
+}
+
+#graph-controls .fa {
+    font-size: 18px;
+    margin-left: -2px;
+}
+
+.graph-control-header-icon.fa {
+    color: #004849; /*link-color*/
+    margin-left: 7px !important;
+}
+
+div.graph-control {
+    box-shadow: 0 1px 6px rgba(0,0,0,0.25);
+    background-color: rgba(249, 250, 251, 0.9);
+    border-top: 1px solid #aabbc3;
+    border-right: 1px solid #aabbc3;
+    border-bottom: 1px solid #aabbc3;
+    margin-bottom: 2px;
+}
+
+.graph-control-content {
+    margin-left: 10px;
+    margin-right: 10px;
+    margin-bottom: 10px;
+}
+
+.docked {
+    height: 32px;
+    width: 32px;
+}
+
+div.graph-control-docked {
+    height: 100%;
+    width: 100%;
+    text-align: center;
+    line-height: 34px;
+    color: #004849;
+}
+
+.docked:hover {
+    border-top: 1px solid #004849; /*tint base-color 60%*/
+    border-right: 1px solid #004849; /*tint base-color 60%*/
+    border-bottom: 1px solid #004849; /*tint base-color 60%*/
+}
+
+div.graph-control button {
+    line-height: 30px;
+    border: 1px solid #CCDADB; /*tint link-color 80%*/
+    background-color: rgba(249,250,251,1);
+    color: #004849;
+}
+
+div.graph-control button:hover {
+    border: 1px solid #004849; /*link-color*/
+}
+
+div.graph-control button:disabled {
+    color: #CCDADB; /*tint link-color 80%*/
+    cursor: not-allowed;
+    border: 1px solid #CCDADB; /*tint link-color 80%*/
+}
+
+div.graph-control div.graph-control-expansion {
+    color: #728E9B;
+    line-height: 34px;
+    margin-left: 9px !important;
+}
+
+div.graph-control-header-icon {
+    float: left;
+    margin: 8px 10px 0px 0px;
+}
+
+div.graph-control-header {
+    float: left;
+    font-size: 12px;
+    font-family: 'Roboto Slab';
+    color: #262626;
+    letter-spacing: 0.05rem;
+    margin: 10px 0px;
+}
+
+div.graph-control-header-action {
+    float: right;
+    height: 32px;
+    width: 32px;
+}
+
+.graph-control-header-container:hover {
+    background: linear-gradient(90deg, rgba(227,232,235,0) 254px, rgba(227,232,235,1) 32px);
+}
+
+/* navigate buttons */
+
+#navigation-buttons {
+    margin-bottom: 5px;
+    margin-top: 10px;
+}
+
+#operation-context {
+    margin-top: 10px;
+}
+
+#operation-context-logo {
+    float: left;
+}
+
+#operation-context-logo i.icon {
+    font-size: 32px;
+    font-family: flowfont;
+    color: #ad9897;
+}
+
+#operation-context-details-container {
+    float: left;
+    padding-left: 10px;
+}
+
+#operation-context-name {
+    height: 15px;
+    font-size: 15px;
+    font-family: Roboto;
+    color: #262626;
+    width: 230px;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+}
+
+#operation-context-type {
+    font-size: 12px;
+    font-family: Roboto;
+    color: #728e9b;
+    margin-top: 3px;
+}
+
+#operation-context-id {
+    font-size: 12px;
+    font-family: Roboto;
+    color: #775351;
+    margin-top: 10px;
+}
+
+#operation-context-type.invisible, #operation-context-id.invisible {
+    visibility: hidden;
+}
+
+#operation-buttons {
+    margin-top: 10px;
+}
+
+div.action-button {
+    float: left;
+}
+
+
+#operate-delete button {
+    width: inherit;
+    padding: 0 7px;
+}
+
+#operate-delete button span{
+    padding-left: 5px;
+    font-size: 12px;
+ }
+
+#operate-submit button {
+    width: inherit;
+    padding: 0 7px;
+}
+
+ #operate-submit button span{
+     padding-left: 5px;
+     font-size: 12px;
+     color: green;
+  }
+
+#operate-refresh button {
+    width: inherit;
+    padding: 0 7px;
+}
+
+ #operate-refresh button span{
+     padding-left: 5px;
+     font-size: 12px;
+     color: blue;
+  }
+
+div.graph-control div.icon-disabled {
+    color: #ddd;
+}
+
+div.button-spacer-small {
+    float: left;
+    width: 2px;
+}
+
+div.button-spacer-large {
+    float: left;
+    width: 12px;
+}
+
+/* outline/birdseye */
+
+#birdseye svg, #birdseye canvas {
+    position: absolute;
+    overflow: hidden;
+}
+
+#birdseye {
+    width: 264px;
+    height: 150px;
+    background: #fff;
+    z-index: 1001;
+    overflow: hidden;
+    border: 1px solid #e5ebed;
+}
+
+.brush .selection {
+    stroke: #666;
+    fill-opacity: .125;
+    shape-rendering: crispEdges;
+}
+
+rect.birdseye-brush {
+    stroke: #7098ad;
+    fill: transparent;
+}
+
+/* styles for the breadcrumbs bar */
+
+#breadcrumbs {
+    position: absolute;
+    bottom: 0px;
+    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
+    background-color: rgba(249, 250, 251, 0.9);
+    border-top: 1px solid #aabbc3;
+    color: #598599;
+    z-index: 3;
+    height: 31px;
+    width: 100%;
+}
+
+#cluster-indicator {
+    width: 49px;
+    height: 15px;
+    background-color: transparent;
+    display: none;
+    position: absolute;
+    left: 59px;
+    top: 8px;
+}
+
+span.breadcrumb-version-control-green {
+    color: #1a9964;
+}
+
+span.breadcrumb-version-control-red {
+    color: #ba554a;
+}
+
+span.breadcrumb-version-control-gray {
+    color: #666666;
+}
+
+#breadcrumbs-left-border {
+    position: absolute;
+    left: 0;
+    width: 10px;
+    height: 14px;
+    z-index: 3;
+    background-color: transparent;
+    background: linear-gradient(to right, rgba(249, 250, 251, 0.97), rgba(255, 255, 255, 0));
+    filter: progid:DXImageTransform.Microsoft.gradient(gradientType=1, startColorstr=#ffffffff, endColorstr=#00ffffff);
+}
+
+#breadcrumbs-right-border {
+    position: absolute;
+    right: 0px;
+    width: 10px;
+    height: 14px;
+    z-index: 3;
+    background-color: transparent;
+    background: linear-gradient(to left, rgba(249, 250, 251, 0.97), rgba(255, 255, 255, 0));
+    filter: progid:DXImageTransform.Microsoft.gradient(gradientType=1, startColorstr=#00ffffff, endColorstr=#ffffffff);
+}
+
+#data-flow-title-viewport {
+    overflow: hidden;
+    position: absolute;
+    left: 5px;
+    top: 8px;
+    right: 5px;
+    z-index: 4;
+}
+
+#data-flow-title-container {
+    font-size: 13px;
+    color: #000;
+    position: relative;
+    float: left;
+    white-space: nowrap;
+    line-height: normal;
+}
diff --git a/mod/designtool/designtool-web/src/main/webapp/images/dcae-logo.png b/mod/designtool/designtool-web/src/main/webapp/images/dcae-logo.png
new file mode 100644 (file)
index 0000000..2e0d5a0
Binary files /dev/null and b/mod/designtool/designtool-web/src/main/webapp/images/dcae-logo.png differ
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/jquery/dcae-mod.js b/mod/designtool/designtool-web/src/main/webapp/js/jquery/dcae-mod.js
new file mode 100644 (file)
index 0000000..879739c
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+============LICENSE_START=======================================================
+Copyright (c) 2019 AT&T Intellectual Property. All rights reserved.
+================================================================================
+Licensed 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.
+============LICENSE_END=========================================================
+*/
+
+console.log("loading dcae-mod");
+
+    var dt_id;
+    var hostname;
+
+    /**
+    * @desc: on load of page, makes submit button disabled. Also makes an api call to get the host IP of the current instance
+    */
+    $(document).ready(function (){
+        if(dt_id == null){   $('#operate-submit-btn').prop('disabled', true);   }
+
+        //get hostname
+        $.ajax({
+               type: 'GET',
+               url:   '../nifi-api/flow/config',
+               dataType: 'json',
+               contentType: 'application/json',
+               success: function(data){
+                    hostname= data.flowConfiguration.dcaeDistributorApiHostname;
+
+                   //function call: invokes api to refresh the list of Envs
+                    if(hostname){    getDistributionTargets();   }
+                  }
+          });
+    });
+
+   /**
+    * common function to reuse : invokes api to get new updates list environments.
+    * @desc: Makes the select dropdown empty first. Then manually add Placeholder as first/default Option.
+    *        And then dynamically add list of Environments as Options.
+    */
+    function getDistributionTargets(){
+        var select = document.getElementById("environmentType");
+         if(select && select.options && select.options.length > 0){
+             select.options.length=0;
+             var element= document.createElement("option");
+             element.textContent= "Select Environment";
+             element.selected=true;
+             element.disabled=true;
+             element.className="combo-option-text";
+             select.appendChild(element);
+          }else{  select=[];   }
+
+           $.ajax({
+                   type: 'GET',
+                   url:  hostname+'/distribution-targets',
+                   dataType: 'json',
+                   contentType: 'application/json',
+                   success: function(data){
+                    if(data){
+                           for(var i=0; i < data.distributionTargets.length; i++){
+                             var opt= data.distributionTargets[i];
+                             var element= document.createElement("option");
+                              element.textContent= opt.name;
+                              element.value= opt.id;
+                              element.className="combo-option-text";
+                              select.appendChild(element);
+                           }
+                    }
+                  }
+           })
+    }
+
+    /**
+    * @desc: submit button functionality to distribute/POST process group to the environment.
+    */
+     var distributeGraph = function(){
+        var selected_id = $('#operation-context-id').text();
+        // process group id (nifi api) != flow id (nifi registry api)
+        // so must first fetch the flow id from nifi api
+        $.ajax({
+          type: 'GET',
+          url: '../nifi-api/process-groups/'+selected_id,
+          contentType: 'application/json',
+          success: function(data) {
+            const flow_id = data["component"]["versionControlInformation"]["flowId"];
+            const request = {"processGroupId": flow_id}
+
+            $.ajax({
+                    type: 'POST',
+                    data: JSON.stringify(request),
+                    url:  hostname+'/distribution-targets/'+dt_id+'/process-groups',
+                    dataType: 'json',
+                    contentType: 'application/json',
+                    success: function(data){
+                         alert("Success, Your flow have been distributed successfully");
+                    },
+                    error: function(err) {
+                        alert("Issue with distribution:\n\n" + JSON.stringify(err, null, 2));
+                    }
+            });
+          }
+        })
+     };
+
+
+   /**
+   * @desc: selection of distribution target environment to post the process group
+   */
+   var onEnvironmentSelect = function(){
+     dt_id = $('#environmentType').val();
+     console.log(dt_id);
+     if(dt_id == null){   $('#operate-submit-btn').prop('disabled', true);   }
+     else{  $('#operate-submit-btn').prop('disabled', false);      }
+    };
+
+
+    /**
+    * @desc: event handler for Refresh icon in Operate panel :  invokes api to refresh the list of Envs
+    */
+    var refreshEnvironments= function(){    getDistributionTargets();    };
+
+
+    /**
+    * @desc: event handler for Close icon of Setting/ Distribution Env CRUD dialog :  invokes api to refresh the list of Envs
+    */
+    var onCloseSettings= function(){  getDistributionTargets();  };
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js
new file mode 100644 (file)
index 0000000..0005837
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* global define, module, require, exports */
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'nf.Common'],
+            function ($, nfCommon) {
+                return (nf.ng.BreadcrumbsCtrl = factory($, nfCommon));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.ng.BreadcrumbsCtrl =
+            factory(require('jquery'),
+                require('nf.Common')));
+    } else {
+        nf.ng.BreadcrumbsCtrl = factory(root.$,
+            root.nf.Common);
+    }
+}(this, function ($, nfCommon) {
+    'use strict';
+
+    return function (serviceProvider) {
+        'use strict';
+
+        function BreadcrumbsCtrl() {
+            this.breadcrumbs = [];
+        }
+
+        BreadcrumbsCtrl.prototype = {
+            constructor: BreadcrumbsCtrl,
+
+            /**
+             *  Register the breadcrumbs controller.
+             */
+            register: function () {
+                if (serviceProvider.breadcrumbsCtrl === undefined) {
+                    serviceProvider.register('breadcrumbsCtrl', breadcrumbsCtrl);
+                }
+            },
+
+            /**
+             * Generate the breadcrumbs.
+             *
+             * @param {object} breadcrumbEntity  The breadcrumb
+             */
+            generateBreadcrumbs: function (breadcrumbEntity) {
+                var label = breadcrumbEntity.id;
+                if (breadcrumbEntity.permissions.canRead) {
+                    label = breadcrumbEntity.breadcrumb.name;
+                }
+
+                this.breadcrumbs.unshift($.extend({
+                    'label': label
+                }, breadcrumbEntity));
+
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.parentBreadcrumb)) {
+                    this.generateBreadcrumbs(breadcrumbEntity.parentBreadcrumb);
+                }
+            },
+
+            /**
+             * 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 () {
+                this.breadcrumbs = [];
+            },
+
+            /**
+             * Whether this crumb is tracking.
+             *
+             * @param breadcrumbEntity
+             * @returns {*}
+             */
+            isTracking: function (breadcrumbEntity) {
+                return nfCommon.isDefinedAndNotNull(breadcrumbEntity.versionedFlowState);
+            },
+
+            /**
+             * Returns the class string to use for the version control of the specified breadcrumb.
+             *
+             * @param breadcrumbEntity
+             * @returns {string}
+             */
+            getVersionControlClass: function (breadcrumbEntity) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.versionedFlowState)) {
+                    var vciState = breadcrumbEntity.versionedFlowState;
+                    if (vciState === 'SYNC_FAILURE') {
+                             console.log("it is been sync failed..000");
+                                 $('#environmentType').prop('disabled', true);
+                                              if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                                     $('#operate-submit-btn').prop('disabled', false);
+                                              }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                        return 'breadcrumb-version-control-gray fa fa-question'
+                    } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
+                       console.log("it is been locally modified and stale...000");
+                                      $('#environmentType').prop('disabled', true);
+                                            if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                                   $('#operate-submit-btn').prop('disabled', false);
+                                            }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                        return 'breadcrumb-version-control-red fa fa-exclamation-circle';
+                    } else if (vciState === 'STALE') {
+                            console.log("it is been stale...000");
+                                        $('#environmentType').prop('disabled', true);
+                                            if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                                   $('#operate-submit-btn').prop('disabled', false);
+                                            }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                        return 'breadcrumb-version-control-red fa fa-arrow-circle-up';
+                    } else if (vciState === 'LOCALLY_MODIFIED') {
+                            console.log("it is been locally modified...000");
+                            $('#environmentType').prop('disabled', true);
+                            if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                    $('#operate-submit-btn').prop('disabled', false);
+                            }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                        return 'breadcrumb-version-control-gray fa fa-asterisk';
+                    } else {
+                             $('#environmentType').prop('disabled', false);
+                             if($('#environmentType').val() &&  !$('#environmentType').prop('disabled')){
+                                   $('#operate-submit-btn').prop('disabled', false);
+                             }else{$('#operate-submit-btn').prop('disabled', true);}
+                        return 'breadcrumb-version-control-green fa fa-check';
+                    }
+                } else {
+                    console.log("it is NOT been version controlled...000");
+                    $('#environmentType').prop('disabled', true);
+                    return '';
+                }
+            },
+
+            /**
+             * Gets the content for the version control tooltip for the specified breadcrumb.
+             *
+             * @param breadcrumbEntity
+             */
+            getVersionControlTooltip: function (breadcrumbEntity) {
+                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.versionedFlowState) && breadcrumbEntity.permissions.canRead) {
+                    return nfCommon.getVersionControlTooltip(breadcrumbEntity.breadcrumb.versionControlInformation);
+                } else {
+                    return 'This Process Group is not under version control.'
+                }
+            },
+
+            /**
+             * Get the breadcrumbs.
+             */
+            getBreadcrumbs: function () {
+                return this.breadcrumbs;
+            },
+
+            /**
+             * Update the breadcrumbs css.
+             *
+             * @param {object} style  The style to be applied.
+             */
+            updateBreadcrumbsCss: function (style) {
+                $('#breadcrumbs').css(style);
+            },
+
+            /**
+             * Reset initial scroll position.
+             */
+            resetScrollPosition: function () {
+                var title = $('#data-flow-title-container');
+                var titlePosition = title.position();
+                var titleWidth = title.outerWidth();
+                var titleRight = titlePosition.left + titleWidth;
+
+                var padding = $('#breadcrumbs-right-border').width();
+                var viewport = $('#data-flow-title-viewport');
+                var viewportWidth = viewport.width();
+                var viewportRight = viewportWidth - padding;
+
+                // if the title's right is past the viewport's right, shift accordingly
+                if (titleRight > viewportRight) {
+                    // adjust the position
+                    title.css('left', (titlePosition.left - (titleRight - viewportRight)) + 'px');
+                } else {
+                    title.css('left', '10px');
+                }
+            },
+
+            /**
+             * Registers a scroll event on the `element`
+             *
+             * @param {object} element    The element event listener will be registered upon.
+             */
+            registerMouseWheelEvent: function (element) {
+                // mousewheel -> IE, Chrome
+                // DOMMouseScroll -> FF
+                // wheel -> FF, IE
+
+                // still having issues with this in IE :/
+                element.on('DOMMouseScroll mousewheel', function (evt, d) {
+                    if (nfCommon.isUndefinedOrNull(evt.originalEvent)) {
+                        return;
+                    }
+
+                    var title = $('#data-flow-title-container');
+                    var titlePosition = title.position();
+                    var titleWidth = title.outerWidth();
+                    var titleRight = titlePosition.left + titleWidth;
+
+                    var padding = $('#breadcrumbs-right-border').width();
+                    var viewport = $('#data-flow-title-viewport');
+                    var viewportWidth = viewport.width();
+                    var viewportRight = viewportWidth - padding;
+
+                    // if the width of the title is larger than the viewport
+                    if (titleWidth > viewportWidth) {
+                        var adjust = false;
+
+                        var delta = 0;
+
+                        //Chrome and Safari both have evt.originalEvent.detail defined but
+                        //evt.originalEvent.wheelDelta holds the correct value so we must
+                        //check for evt.originalEvent.wheelDelta first!
+                        if (nfCommon.isDefinedAndNotNull(evt.originalEvent.wheelDelta)) {
+                            delta = evt.originalEvent.wheelDelta;
+                        } else if (nfCommon.isDefinedAndNotNull(evt.originalEvent.detail)) {
+                            delta = -evt.originalEvent.detail;
+                        }
+
+                        // determine the increment
+                        if (delta > 0 && titleRight > viewportRight) {
+                            var increment = -25;
+                            adjust = true;
+                        } else if (delta < 0 && (titlePosition.left - padding) < 0) {
+                            increment = 25;
+
+                            // don't shift too far
+                            if (titlePosition.left + increment > padding) {
+                                increment = padding - titlePosition.left;
+                            }
+
+                            adjust = true;
+                        }
+
+                        if (adjust) {
+                            // adjust the position
+                            title.css('left', (titlePosition.left + increment) + 'px');
+                        }
+                    }
+                });
+            }
+        }
+
+        var breadcrumbsCtrl = new BreadcrumbsCtrl();
+        breadcrumbsCtrl.register();
+        return breadcrumbsCtrl;
+    }
+}));
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/header/components/nf-ng-processor-component.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/header/components/nf-ng-processor-component.js
new file mode 100644 (file)
index 0000000..0f4b953
--- /dev/null
@@ -0,0 +1,1150 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* global define, module, require, exports */
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'Slick',
+                'nf.Client',
+                'nf.Birdseye',
+                'nf.Storage',
+                'nf.Graph',
+                'nf.CanvasUtils',
+                'nf.ErrorHandler',
+                'nf.FilteredDialogCommon',
+                'nf.Dialog',
+                'nf.Common'],
+            function ($, Slick, nfClient, nfBirdseye, nfStorage, nfGraph, nfCanvasUtils, nfErrorHandler, nfFilteredDialogCommon, nfDialog, nfCommon) {
+                return (nf.ng.ProcessorComponent = factory($, Slick, nfClient, nfBirdseye, nfStorage, nfGraph, nfCanvasUtils, nfErrorHandler, nfFilteredDialogCommon, nfDialog, nfCommon));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.ng.ProcessorComponent =
+            factory(require('jquery'),
+                require('Slick'),
+                require('nf.Client'),
+                require('nf.Birdseye'),
+                require('nf.Storage'),
+                require('nf.Graph'),
+                require('nf.CanvasUtils'),
+                require('nf.ErrorHandler'),
+                require('nf.FilteredDialogCommon'),
+                require('nf.Dialog'),
+                require('nf.Common')));
+    } else {
+        nf.ng.ProcessorComponent = factory(root.$,
+            root.Slick,
+            root.nf.Client,
+            root.nf.Birdseye,
+            root.nf.Storage,
+            root.nf.Graph,
+            root.nf.CanvasUtils,
+            root.nf.ErrorHandler,
+            root.nf.FilteredDialogCommon,
+            root.nf.Dialog,
+            root.nf.Common);
+    }
+}(this, function ($, Slick, nfClient, nfBirdseye, nfStorage, nfGraph, nfCanvasUtils, nfErrorHandler, nfFilteredDialogCommon, nfDialog, nfCommon) {
+    'use strict';
+
+    return function (serviceProvider) {
+        'use strict';
+
+
+        var latestResponse;
+
+        var processorTypesData;
+
+
+        /**
+         * Filters the processor type table.
+         */
+        var applyFilter = function () {
+            // get the dataview
+            var processorTypesGrid = $('#processor-types-table').data('gridInstance');
+
+            // ensure the grid has been initialized
+            if (nfCommon.isDefinedAndNotNull(processorTypesGrid)) {
+                var processorTypesDataForFilter = processorTypesGrid.getData();
+
+                // update the search criteria
+                processorTypesDataForFilter.setFilterArgs({
+                    searchString: getFilterText()
+                });
+                processorTypesDataForFilter.refresh();
+
+                // update the buttons to possibly trigger the disabled state
+                $('#new-processor-dialog').modal('refreshButtons');
+
+                // update the selection if possible
+                if (processorTypesDataForFilter.getLength() > 0) {
+                    nfFilteredDialogCommon.choseFirstRow(processorTypesGrid);
+                    // make the first row visible
+                    processorTypesGrid.scrollRowToTop(0);
+                }
+            }
+        };
+
+        /**
+         * Determines if the item matches the filter.
+         *
+         * @param {object} item     The item to filter.
+         * @param {object} args     The filter criteria.
+         * @returns {boolean}       Whether the item matches the filter.
+         */
+        var matchesRegex = 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 matchesLabel = item['label'].search(filterExp) >= 0;
+            var matchesTags = item['tags'].search(filterExp) >= 0;
+            return matchesLabel || matchesTags;
+        };
+
+        /**
+         * Performs the filtering.
+         *
+         * @param {object} item     The item subject to filtering.
+         * @param {object} args     Filter arguments.
+         * @returns {Boolean}       Whether or not to include the item.
+         */
+        var filter = function (item, args) {
+            // determine if the item matches the filter
+            var matchesFilter = matchesRegex(item, args);
+
+            // determine if the row matches the selected tags
+            var matchesTags = true;
+            if (matchesFilter) {
+                var tagFilters = $('#processor-tag-cloud').tagcloud('getSelectedTags');
+                var hasSelectedTags = tagFilters.length > 0;
+                if (hasSelectedTags) {
+                    matchesTags = matchesSelectedTags(tagFilters, item['tags']);
+                }
+            }
+
+            // determine if the row matches the selected source group
+            var matchesGroup = true;
+            if (matchesFilter && matchesTags) {
+                var bundleGroup = $('#processor-bundle-group-combo').combo('getSelectedOption');
+                if (nfCommon.isDefinedAndNotNull(bundleGroup) && bundleGroup.value !== '') {
+                    matchesGroup = (item.bundle.group === bundleGroup.value);
+                }
+            }
+
+            // determine if this row should be visible
+            var matches = matchesFilter && matchesTags && matchesGroup;
+
+            // if this row is currently selected and its being filtered
+            if (matches === false && $('#selected-processor-type').text() === item['type']) {
+                // clear the selected row
+                $('#processor-type-description').attr('title', '').text('');
+                $('#processor-type-name').attr('title', '').text('');
+                $('#processor-type-bundle').attr('title', '').text('');
+                $('#selected-processor-name').text('');
+                $('#selected-processor-type').text('').removeData('bundle');
+
+                // clear the active cell the it can be reselected when its included
+                var processTypesGrid = $('#processor-types-table').data('gridInstance');
+                processTypesGrid.resetActiveCell();
+            }
+
+            return matches;
+        };
+
+        /**
+         * Determines if the specified tags match all the tags selected by the user.
+         *
+         * @argument {string[]} tagFilters      The tag filters.
+         * @argument {string} tags              The tags to test.
+         */
+        var matchesSelectedTags = function (tagFilters, tags) {
+            var selectedTags = [];
+            $.each(tagFilters, function (_, filter) {
+                selectedTags.push(filter);
+            });
+
+            // normalize the tags
+            var normalizedTags = tags.toLowerCase();
+
+            var matches = true;
+            $.each(selectedTags, function (i, selectedTag) {
+                if (normalizedTags.indexOf(selectedTag) === -1) {
+                    matches = false;
+                    return false;
+                }
+            });
+
+            return matches;
+        };
+
+        /**
+         * Get the text out of the filter field. If the filter field doesn't
+         * have any text it will contain the text 'filter list' so this method
+         * accounts for that.
+         */
+        var getFilterText = function () {
+            return $('#processor-type-filter').val();
+        };
+
+        /**
+         * Resets the filtered processor types.
+         */
+        var resetProcessorDialog = function () {
+
+ //********* REPLICATED A BLOCK OF CODE to get logic of autoloading processor ---STARTING FROM HERE----********************
+
+                                // initialize the processor type table
+                                var processorTypesColumns = [
+                                    {
+                                        id: 'type',
+                                        name: 'Type',
+                                        field: 'label',
+                                        formatter: nfCommon.typeFormatter,
+                                        sortable: true,
+                                        resizable: true
+                                    },
+                                    {
+                                        id: 'version',
+                                        name: 'Version',
+                                        field: 'version',
+                                        formatter: nfCommon.typeVersionFormatter,
+                                        sortable: true,
+                                        resizable: true
+                                    },
+                                    {
+                                        id: 'tags',
+                                        name: 'Tags',
+                                        field: 'tags',
+                                        sortable: true,
+                                        resizable: true,
+                                        formatter: nfCommon.genericValueFormatter
+                                    }
+                                ];
+
+                                var processorTypesOptions = {
+                                    forceFitColumns: true,
+                                    enableTextSelectionOnCells: true,
+                                    enableCellNavigation: true,
+                                    enableColumnReorder: false,
+                                    autoEdit: false,
+                                    multiSelect: false,
+                                    rowHeight: 24
+                                };
+
+                                // initialize the dataview
+                                processorTypesData = new Slick.Data.DataView({
+                                    inlineFilters: false
+                                });
+                                processorTypesData.setItems([]);
+                                processorTypesData.setFilterArgs({
+                                    searchString: getFilterText()
+                                });
+                                processorTypesData.setFilter(filter);
+
+                                // initialize the sort
+                                nfCommon.sortType({
+                                    columnId: 'type',
+                                    sortAsc: true
+                                }, processorTypesData);
+
+                                // initialize the grid
+                                var processorTypesGrid = new Slick.Grid('#processor-types-table', processorTypesData, processorTypesColumns, processorTypesOptions);
+                                processorTypesGrid.setSelectionModel(new Slick.RowSelectionModel());
+                                processorTypesGrid.registerPlugin(new Slick.AutoTooltips());
+                                processorTypesGrid.setSortColumn('type', true);
+                                processorTypesGrid.onSort.subscribe(function (e, args) {
+                                    nfCommon.sortType({
+                                        columnId: args.sortCol.field,
+                                        sortAsc: args.sortAsc
+                                    }, processorTypesData);
+                                });
+                                processorTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+                                    if ($.isArray(args.rows) && args.rows.length === 1) {
+                                        var processorTypeIndex = args.rows[0];
+                                        var processorType = processorTypesGrid.getDataItem(processorTypeIndex);
+
+                                        // set the processor type description
+                                        if (nfCommon.isDefinedAndNotNull(processorType)) {
+                                            if (nfCommon.isBlank(processorType.description)) {
+                                                $('#processor-type-description')
+                                                    .attr('title', '')
+                                                    .html('<span class="unset">No description specified</span>');
+                                            } else {
+                                                $('#processor-type-description')
+                                                    .width($('#processor-description-container').innerWidth() - 1)
+                                                    .html(processorType.description)
+                                                    .ellipsis();
+                                            }
+
+                                            var bundle = nfCommon.formatBundle(processorType.bundle);
+                                            var type = nfCommon.formatType(processorType);
+
+                                            // populate the dom
+                                            $('#processor-type-name').text(type).attr('title', type);
+                                            $('#processor-type-bundle').text(bundle).attr('title', bundle);
+                                            $('#selected-processor-name').text(processorType.label);
+                                            $('#selected-processor-type').text(processorType.type).data('bundle', processorType.bundle);
+
+                                            // refresh the buttons based on the current selection
+                                            $('#new-processor-dialog').modal('refreshButtons');
+                                        }
+                                    }
+                                });
+                                processorTypesGrid.onViewportChanged.subscribe(function (e, args) {
+                                    nfCommon.cleanUpTooltips($('#processor-types-table'), 'div.view-usage-restriction');
+                                });
+
+                                // wire up the dataview to the grid
+                                processorTypesData.onRowCountChanged.subscribe(function (e, args) {
+                                    processorTypesGrid.updateRowCount();
+                                    processorTypesGrid.render();
+
+                                    // update the total number of displayed processors
+                                    $('#displayed-processor-types').text(args.current);
+                                });
+                                processorTypesData.onRowsChanged.subscribe(function (e, args) {
+                                    processorTypesGrid.invalidateRows(args.rows);
+                                    processorTypesGrid.render();
+                                });
+                                processorTypesData.syncGridSelection(processorTypesGrid, false);
+
+                                // hold onto an instance of the grid
+                                $('#processor-types-table').data('gridInstance', processorTypesGrid).on('mouseenter', 'div.slick-cell', function (e) {
+                                    var usageRestriction = $(this).find('div.view-usage-restriction');
+                                    if (usageRestriction.length && !usageRestriction.data('qtip')) {
+                                        var rowId = $(this).find('span.row-id').text();
+
+                                        // get the status item
+                                        var item = processorTypesData.getItemById(rowId);
+
+                                        // show the tooltip
+                                        if (item.restricted === true) {
+                                            var restrictionTip = $('<div></div>');
+
+                                            if (nfCommon.isBlank(item.usageRestriction)) {
+                                                restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text('Requires the following permissions:'));
+                                            } else {
+                                                restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text(item.usageRestriction + ' Requires the following permissions:'));
+                                            }
+
+                                            var restrictions = [];
+                                            if (nfCommon.isDefinedAndNotNull(item.explicitRestrictions)) {
+                                                $.each(item.explicitRestrictions, function (_, explicitRestriction) {
+                                                    var requiredPermission = explicitRestriction.requiredPermission;
+                                                    restrictions.push("'" + requiredPermission.label + "' - " + nfCommon.escapeHtml(explicitRestriction.explanation));
+                                                });
+                                            } else {
+                                                restrictions.push('Access to restricted components regardless of restrictions.');
+                                            }
+                                            restrictionTip.append(nfCommon.formatUnorderedList(restrictions));
+
+                                            usageRestriction.qtip($.extend({}, nfCommon.config.tooltipConfig, {
+                                                content: restrictionTip,
+                                                position: {
+                                                    container: $('#summary'),
+                                                    at: 'bottom right',
+                                                    my: 'top left',
+                                                    adjust: {
+                                                        x: 4,
+                                                        y: 4
+                                                    }
+                                                }
+                                            }));
+                                        }
+                                    }
+                                });
+
+                                var generalRestriction = nfCommon.getPolicyTypeListing('restricted-components');
+
+                                // load the available processor types, this select is shown in the
+                                // new processor dialog when a processor is dragged onto the screen
+                                $.ajax({
+                                    type: 'GET',
+                                    url:'../nifi-api/flow/processor-types',
+        //                            url: serviceProvider.headerCtrl.toolboxCtrl.config.urls.processorTypes,
+                                    dataType: 'json'
+                                }).done(function (response) {
+                                    console.log(response);
+                                    var tags = [];
+                                    var groups = d3.set();
+                                    var restrictedUsage = d3.map();
+                                    var requiredPermissions = d3.map();
+
+                                    // begin the update
+                                    processorTypesData.beginUpdate();
+
+                                    // go through each processor type
+                                    $.each(response.processorTypes, function (i, documentedType) {
+                                        var type = documentedType.type;
+
+                                        if (documentedType.restricted === true) {
+                                            if (nfCommon.isDefinedAndNotNull(documentedType.explicitRestrictions)) {
+                                                $.each(documentedType.explicitRestrictions, function (_, explicitRestriction) {
+                                                    var requiredPermission = explicitRestriction.requiredPermission;
+
+                                                    // update required permissions
+                                                    if (!requiredPermissions.has(requiredPermission.id)) {
+                                                        requiredPermissions.set(requiredPermission.id, requiredPermission.label);
+                                                    }
+
+                                                    // update component restrictions
+                                                    if (!restrictedUsage.has(requiredPermission.id)) {
+                                                        restrictedUsage.set(requiredPermission.id, []);
+                                                    }
+
+                                                    restrictedUsage.get(requiredPermission.id).push({
+                                                        type: nfCommon.formatType(documentedType),
+                                                        bundle: nfCommon.formatBundle(documentedType.bundle),
+                                                        explanation: nfCommon.escapeHtml(explicitRestriction.explanation)
+                                                    })
+                                                });
+                                            } else {
+                                                // update required permissions
+                                                if (!requiredPermissions.has(generalRestriction.value)) {
+                                                    requiredPermissions.set(generalRestriction.value, generalRestriction.text);
+                                                }
+
+                                                // update component restrictions
+                                                if (!restrictedUsage.has(generalRestriction.value)) {
+                                                    restrictedUsage.set(generalRestriction.value, []);
+                                                }
+
+                                                restrictedUsage.get(generalRestriction.value).push({
+                                                    type: nfCommon.formatType(documentedType),
+                                                    bundle: nfCommon.formatBundle(documentedType.bundle),
+                                                    explanation: nfCommon.escapeHtml(documentedType.usageRestriction)
+                                                });
+                                            }
+                                        }
+
+                                        // record the group
+                                        groups.add(documentedType.bundle.group);
+
+                                        // create the row for the processor type
+                                        processorTypesData.addItem({
+                                            id: i,
+                                            label: nfCommon.substringAfterLast(type, '.'),
+                                            type: type,
+                                            bundle: documentedType.bundle,
+                                            description: nfCommon.escapeHtml(documentedType.description),
+                                            restricted:  documentedType.restricted,
+                                            usageRestriction: nfCommon.escapeHtml(documentedType.usageRestriction),
+                                            explicitRestrictions: documentedType.explicitRestrictions,
+                                            tags: documentedType.tags.join(', ')
+                                        });
+
+                                        // count the frequency of each tag for this type
+                                        $.each(documentedType.tags, function (i, tag) {
+                                            tags.push(tag.toLowerCase());
+                                        });
+                                    });
+
+                                    // end the update
+                                    processorTypesData.endUpdate();
+
+                                    // resort
+                                    processorTypesData.reSort();
+                                    processorTypesGrid.invalidate();
+
+                                    // set the component restrictions and the corresponding required permissions
+                                    nfCanvasUtils.addComponentRestrictions(restrictedUsage, requiredPermissions);
+
+                                    // set the total number of processors
+                                    $('#total-processor-types, #displayed-processor-types').text(response.processorTypes.length);
+
+                                    // create the tag cloud
+                                    $('#processor-tag-cloud').tagcloud({
+                                        tags: tags,
+                                        select: applyFilter,
+                                        remove: applyFilter
+                                    });
+
+                                    // build the combo options
+                                    var options = [{
+                                        text: 'all groups',
+                                        value: ''
+                                    }];
+                                    groups.each(function (group) {
+                                        options.push({
+                                            text: group,
+                                            value: group
+                                        });
+                                    });
+
+                                    // initialize the bundle group combo
+                                    $('#processor-bundle-group-combo').combo({
+                                        options: options,
+                                        select: applyFilter
+                                    });
+                                }).fail(nfErrorHandler.handleAjaxError);
+
+//************* REPLICATED CODE---ENDS HERE------******************
+
+
+
+            // clear the selected tag cloud
+            $('#processor-tag-cloud').tagcloud('clearSelectedTags');
+
+            // reset the group combo
+            $('#processor-bundle-group-combo').combo('setSelectedOption', {
+                value: ''
+            });
+
+            // clear any filter strings
+            $('#processor-type-filter').val('');
+
+            // reapply the filter
+            applyFilter();
+
+            // clear the selected row
+            $('#processor-type-description').attr('title', '').text('');
+            $('#processor-type-name').attr('title', '').text('');
+            $('#processor-type-bundle').attr('title', '').text('');
+            $('#selected-processor-name').text('');
+            $('#selected-processor-type').text('').removeData('bundle');
+
+            // unselect any current selection
+            var processTypesGrid = $('#processor-types-table').data('gridInstance');
+            processTypesGrid.setSelectedRows([]);
+            processTypesGrid.resetActiveCell();
+        };
+
+        /**
+         * Create the processor and add to the graph.
+         *
+         * @argument {string} name              The processor name.
+         * @argument {string} processorType     The processor type.
+         * @argument {object} bundle            The processor bundle.
+         * @argument {object} pt                The point that the processor was dropped.
+         */
+        var createProcessor = function (name, processorType, bundle, pt) {
+            var processorEntity = {
+                'revision': nfClient.getRevision({
+                    'revision': {
+                        'version': 0
+                    }
+                }),
+                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+                'component': {
+                    'type': processorType,
+                    'bundle': bundle,
+                    'name': name,
+                    'position': {
+                        'x': pt.x,
+                        'y': pt.y
+                    }
+                }
+            };
+
+            // create a new processor of the defined type
+            $.ajax({
+                type: 'POST',
+                url: serviceProvider.headerCtrl.toolboxCtrl.config.urls.api + '/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/processors',
+                data: JSON.stringify(processorEntity),
+                dataType: 'json',
+                contentType: 'application/json'
+            }).done(function (response) {
+                // add the processor to the graph
+                nfGraph.add({
+                    'processors': [response]
+                }, {
+                    'selectAll': true
+                });
+
+                // update component visibility
+                nfGraph.updateVisibility();
+
+                // update the birdseye
+                nfBirdseye.refresh();
+            }).fail(nfErrorHandler.handleAjaxError);
+        };
+
+        /**
+         * Whether the specified item is selectable.
+         *
+         * @param item process type
+         */
+        var isSelectable = function (item) {
+            return item.restricted === false || nfCommon.canAccessComponentRestrictions(item.explicitRestrictions);
+        };
+
+        function ProcessorComponent() {
+
+            this.icon = 'icon icon-processor';
+
+            this.hoverIcon = 'icon icon-processor-add';
+
+            /**
+             * The processor component's modal.
+             */
+            this.modal = {
+
+                /**
+                 * The processor component modal's filter.
+                 */
+                filter: {
+
+                    /**
+                     * Initialize the filter.
+                     */
+                    init: function () {
+                        // initialize the processor type table
+                        var processorTypesColumns = [
+                            {
+                                id: 'type',
+                                name: 'Type',
+                                field: 'label',
+                                formatter: nfCommon.typeFormatter,
+                                sortable: true,
+                                resizable: true
+                            },
+                            {
+                                id: 'version',
+                                name: 'Version',
+                                field: 'version',
+                                formatter: nfCommon.typeVersionFormatter,
+                                sortable: true,
+                                resizable: true
+                            },
+                            {
+                                id: 'tags',
+                                name: 'Tags',
+                                field: 'tags',
+                                sortable: true,
+                                resizable: true,
+                                formatter: nfCommon.genericValueFormatter
+                            }
+                        ];
+
+                        var processorTypesOptions = {
+                            forceFitColumns: true,
+                            enableTextSelectionOnCells: true,
+                            enableCellNavigation: true,
+                            enableColumnReorder: false,
+                            autoEdit: false,
+                            multiSelect: false,
+                            rowHeight: 24
+                        };
+
+                        // initialize the dataview
+                        processorTypesData = new Slick.Data.DataView({
+                            inlineFilters: false
+                        });
+                        processorTypesData.setItems([]);
+                        processorTypesData.setFilterArgs({
+                            searchString: getFilterText()
+                        });
+                        processorTypesData.setFilter(filter);
+
+                        // initialize the sort
+                        nfCommon.sortType({
+                            columnId: 'type',
+                            sortAsc: true
+                        }, processorTypesData);
+
+                        // initialize the grid
+                        var processorTypesGrid = new Slick.Grid('#processor-types-table', processorTypesData, processorTypesColumns, processorTypesOptions);
+                        processorTypesGrid.setSelectionModel(new Slick.RowSelectionModel());
+                        processorTypesGrid.registerPlugin(new Slick.AutoTooltips());
+                        processorTypesGrid.setSortColumn('type', true);
+                        processorTypesGrid.onSort.subscribe(function (e, args) {
+                            nfCommon.sortType({
+                                columnId: args.sortCol.field,
+                                sortAsc: args.sortAsc
+                            }, processorTypesData);
+                        });
+                        processorTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+                            if ($.isArray(args.rows) && args.rows.length === 1) {
+                                var processorTypeIndex = args.rows[0];
+                                var processorType = processorTypesGrid.getDataItem(processorTypeIndex);
+
+                                // set the processor type description
+                                if (nfCommon.isDefinedAndNotNull(processorType)) {
+                                    if (nfCommon.isBlank(processorType.description)) {
+                                        $('#processor-type-description')
+                                            .attr('title', '')
+                                            .html('<span class="unset">No description specified</span>');
+                                    } else {
+                                        $('#processor-type-description')
+                                            .width($('#processor-description-container').innerWidth() - 1)
+                                            .html(processorType.description)
+                                            .ellipsis();
+                                    }
+
+                                    var bundle = nfCommon.formatBundle(processorType.bundle);
+                                    var type = nfCommon.formatType(processorType);
+
+                                    // populate the dom
+                                    $('#processor-type-name').text(type).attr('title', type);
+                                    $('#processor-type-bundle').text(bundle).attr('title', bundle);
+                                    $('#selected-processor-name').text(processorType.label);
+                                    $('#selected-processor-type').text(processorType.type).data('bundle', processorType.bundle);
+
+                                    // refresh the buttons based on the current selection
+                                    $('#new-processor-dialog').modal('refreshButtons');
+                                }
+                            }
+                        });
+                        processorTypesGrid.onViewportChanged.subscribe(function (e, args) {
+                            nfCommon.cleanUpTooltips($('#processor-types-table'), 'div.view-usage-restriction');
+                        });
+
+                        // wire up the dataview to the grid
+                        processorTypesData.onRowCountChanged.subscribe(function (e, args) {
+                            processorTypesGrid.updateRowCount();
+                            processorTypesGrid.render();
+
+                            // update the total number of displayed processors
+                            $('#displayed-processor-types').text(args.current);
+                        });
+                        processorTypesData.onRowsChanged.subscribe(function (e, args) {
+                            processorTypesGrid.invalidateRows(args.rows);
+                            processorTypesGrid.render();
+                        });
+                        processorTypesData.syncGridSelection(processorTypesGrid, false);
+
+                        // hold onto an instance of the grid
+                        $('#processor-types-table').data('gridInstance', processorTypesGrid).on('mouseenter', 'div.slick-cell', function (e) {
+                            var usageRestriction = $(this).find('div.view-usage-restriction');
+                            if (usageRestriction.length && !usageRestriction.data('qtip')) {
+                                var rowId = $(this).find('span.row-id').text();
+
+                                // get the status item
+                                var item = processorTypesData.getItemById(rowId);
+
+                                // show the tooltip
+                                if (item.restricted === true) {
+                                    var restrictionTip = $('<div></div>');
+
+                                    if (nfCommon.isBlank(item.usageRestriction)) {
+                                        restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text('Requires the following permissions:'));
+                                    } else {
+                                        restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text(item.usageRestriction + ' Requires the following permissions:'));
+                                    }
+
+                                    var restrictions = [];
+                                    if (nfCommon.isDefinedAndNotNull(item.explicitRestrictions)) {
+                                        $.each(item.explicitRestrictions, function (_, explicitRestriction) {
+                                            var requiredPermission = explicitRestriction.requiredPermission;
+                                            restrictions.push("'" + requiredPermission.label + "' - " + nfCommon.escapeHtml(explicitRestriction.explanation));
+                                        });
+                                    } else {
+                                        restrictions.push('Access to restricted components regardless of restrictions.');
+                                    }
+                                    restrictionTip.append(nfCommon.formatUnorderedList(restrictions));
+
+                                    usageRestriction.qtip($.extend({}, nfCommon.config.tooltipConfig, {
+                                        content: restrictionTip,
+                                        position: {
+                                            container: $('#summary'),
+                                            at: 'bottom right',
+                                            my: 'top left',
+                                            adjust: {
+                                                x: 4,
+                                                y: 4
+                                            }
+                                        }
+                                    }));
+                                }
+                            }
+                        });
+
+                        var generalRestriction = nfCommon.getPolicyTypeListing('restricted-components');
+
+                        // load the available processor types, this select is shown in the
+                        // new processor dialog when a processor is dragged onto the screen
+                        $.ajax({
+                            type: 'GET',
+                            url:'../nifi-api/flow/processor-types',
+//                            url: serviceProvider.headerCtrl.toolboxCtrl.config.urls.processorTypes,
+                            dataType: 'json'
+                        }).done(function (response) {
+                            console.log(response);
+                            var tags = [];
+                            var groups = d3.set();
+                            var restrictedUsage = d3.map();
+                            var requiredPermissions = d3.map();
+
+                            // begin the update
+                            processorTypesData.beginUpdate();
+
+                            // go through each processor type
+                            $.each(response.processorTypes, function (i, documentedType) {
+                                var type = documentedType.type;
+
+                                if (documentedType.restricted === true) {
+                                    if (nfCommon.isDefinedAndNotNull(documentedType.explicitRestrictions)) {
+                                        $.each(documentedType.explicitRestrictions, function (_, explicitRestriction) {
+                                            var requiredPermission = explicitRestriction.requiredPermission;
+
+                                            // update required permissions
+                                            if (!requiredPermissions.has(requiredPermission.id)) {
+                                                requiredPermissions.set(requiredPermission.id, requiredPermission.label);
+                                            }
+
+                                            // update component restrictions
+                                            if (!restrictedUsage.has(requiredPermission.id)) {
+                                                restrictedUsage.set(requiredPermission.id, []);
+                                            }
+
+                                            restrictedUsage.get(requiredPermission.id).push({
+                                                type: nfCommon.formatType(documentedType),
+                                                bundle: nfCommon.formatBundle(documentedType.bundle),
+                                                explanation: nfCommon.escapeHtml(explicitRestriction.explanation)
+                                            })
+                                        });
+                                    } else {
+                                        // update required permissions
+                                        if (!requiredPermissions.has(generalRestriction.value)) {
+                                            requiredPermissions.set(generalRestriction.value, generalRestriction.text);
+                                        }
+
+                                        // update component restrictions
+                                        if (!restrictedUsage.has(generalRestriction.value)) {
+                                            restrictedUsage.set(generalRestriction.value, []);
+                                        }
+
+                                        restrictedUsage.get(generalRestriction.value).push({
+                                            type: nfCommon.formatType(documentedType),
+                                            bundle: nfCommon.formatBundle(documentedType.bundle),
+                                            explanation: nfCommon.escapeHtml(documentedType.usageRestriction)
+                                        });
+                                    }
+                                }
+
+                                // record the group
+                                groups.add(documentedType.bundle.group);
+
+                                // create the row for the processor type
+                                processorTypesData.addItem({
+                                    id: i,
+                                    label: nfCommon.substringAfterLast(type, '.'),
+                                    type: type,
+                                    bundle: documentedType.bundle,
+                                    description: nfCommon.escapeHtml(documentedType.description),
+                                    restricted:  documentedType.restricted,
+                                    usageRestriction: nfCommon.escapeHtml(documentedType.usageRestriction),
+                                    explicitRestrictions: documentedType.explicitRestrictions,
+                                    tags: documentedType.tags.join(', ')
+                                });
+
+                                // count the frequency of each tag for this type
+                                $.each(documentedType.tags, function (i, tag) {
+                                    tags.push(tag.toLowerCase());
+                                });
+                            });
+
+                            // end the update
+                            processorTypesData.endUpdate();
+
+                            // resort
+                            processorTypesData.reSort();
+                            processorTypesGrid.invalidate();
+
+                            // set the component restrictions and the corresponding required permissions
+                            nfCanvasUtils.addComponentRestrictions(restrictedUsage, requiredPermissions);
+
+                            // set the total number of processors
+                            $('#total-processor-types, #displayed-processor-types').text(response.processorTypes.length);
+
+                            // create the tag cloud
+                            $('#processor-tag-cloud').tagcloud({
+                                tags: tags,
+                                select: applyFilter,
+                                remove: applyFilter
+                            });
+
+                            // build the combo options
+                            var options = [{
+                                text: 'all groups',
+                                value: ''
+                            }];
+                            groups.each(function (group) {
+                                options.push({
+                                    text: group,
+                                    value: group
+                                });
+                            });
+
+                            // initialize the bundle group combo
+                            $('#processor-bundle-group-combo').combo({
+                                options: options,
+                                select: applyFilter
+                            });
+                        }).fail(nfErrorHandler.handleAjaxError);
+                    }
+                },
+
+                /**
+                 * Gets the modal element.
+                 *
+                 * @returns {*|jQuery|HTMLElement}
+                 */
+                getElement: function () {
+                    return $('#new-processor-dialog');
+                },
+
+                /**
+                 * Initialize the modal.
+                 */
+                init: function () {
+                    this.filter.init();
+
+                    // configure the new processor dialog
+                    this.getElement().modal({
+                        scrollableContentStyle: 'scrollable',
+                        headerText: 'Add Processor',
+                        handler: {
+                            resize: function () {
+                                $('#processor-type-description')
+                                    .width($('#processor-description-container').innerWidth() - 1)
+                                    .text($('#processor-type-description').attr('title'))
+                                    .ellipsis();
+                            }
+                        }
+                    });
+                },
+
+                /**
+                 * Updates the modal config.
+                 *
+                 * @param {string} name             The name of the property to update.
+                 * @param {object|array} config     The config for the `name`.
+                 */
+                update: function (name, config) {
+                    this.getElement().modal(name, config);
+                },
+
+                /**
+                 * Show the modal
+                 */
+                show: function () {
+                    this.getElement().modal('show');
+                },
+
+                /**
+                 * Hide the modal
+                 */
+                hide: function () {
+                    this.getElement().modal('hide');
+                }
+            };
+        }
+
+        ProcessorComponent.prototype = {
+            constructor: ProcessorComponent,
+
+            /**
+             * Gets the component.
+             *
+             * @returns {*|jQuery|HTMLElement}
+             */
+            getElement: function () {
+                return $('#processor-component');
+            },
+
+            /**
+             * Enable the component.
+             */
+            enabled: function () {
+                this.getElement().attr('disabled', false);
+            },
+
+            /**
+             * Disable the component.
+             */
+            disabled: function () {
+                this.getElement().attr('disabled', true);
+            },
+
+            /**
+             * Handler function for when component is dropped on the canvas.
+             *
+             * @argument {object} pt        The point that the component was dropped
+             */
+            dropHandler: function (pt) {
+                this.promptForProcessorType(pt);
+            },
+
+            /**
+             * The drag icon for the toolbox component.
+             *
+             * @param event
+             * @returns {*|jQuery|HTMLElement}
+             */
+            dragIcon: function (event) {
+                return $('<div class="icon icon-processor-add"></div>');
+            },
+
+            /**
+             * Prompts the user to select the type of new processor to create.
+             *
+             * @argument {object} pt        The point that the processor was dropped
+             */
+            promptForProcessorType: function (pt) {
+                var processorComponent = this;
+
+                // handles adding the selected processor at the specified point
+                var addProcessor = function () {
+                    // get the type of processor currently selected
+                    var name = $('#selected-processor-name').text();
+                    var processorType = $('#selected-processor-type').text();
+                    var bundle = $('#selected-processor-type').data('bundle');
+
+                    // ensure something was selected
+                    if (name === '' || processorType === '') {
+                        nfDialog.showOkDialog({
+                            headerText: 'Add Processor',
+                            dialogContent: 'The type of processor to create must be selected.'
+                        });
+                    } else {
+                        // create the new processor
+                        createProcessor(name, processorType, bundle, pt);
+                    }
+
+                    // hide the dialog
+                    processorComponent.modal.hide();
+                };
+
+                // get the grid reference
+                var grid = $('#processor-types-table').data('gridInstance');
+                var dataview = grid.getData();
+
+                // add the processor when its double clicked in the table
+                var gridDoubleClick = function (e, args) {
+                    var processorType = grid.getDataItem(args.row);
+
+                    if (isSelectable(processorType)) {
+                        $('#selected-processor-name').text(processorType.label);
+                        $('#selected-processor-type').text(processorType.type).data('bundle', processorType.bundle);
+
+                        addProcessor();
+                    }
+                };
+
+                // register a handler for double click events
+                grid.onDblClick.subscribe(gridDoubleClick);
+
+                // update the button model
+                this.modal.update('setButtonModel', [{
+                    buttonText: 'Add',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    disabled: function () {
+                        var selected = grid.getSelectedRows();
+
+                        if (selected.length > 0) {
+                            // grid configured with multi-select = false
+                            var item = grid.getDataItem(selected[0]);
+                            return isSelectable(item) === false;
+                        } else {
+                            return dataview.getLength() === 0;
+                        }
+                    },
+                    handler: {
+                        click: addProcessor
+                    }
+                },
+                    {
+                        buttonText: 'Cancel',
+                        color: {
+                            base: '#E3E8EB',
+                            hover: '#C7D2D7',
+                            text: '#004849'
+                        },
+                        handler: {
+                            click: function () {
+                                $('#new-processor-dialog').modal('hide');
+                            }
+                        }
+                    }]);
+
+                // set a new handler for closing the the dialog
+                this.modal.update('setCloseHandler', function () {
+                    // remove the handler
+                    grid.onDblClick.unsubscribe(gridDoubleClick);
+
+                    // clear the current filters
+                    resetProcessorDialog();
+                });
+
+                // show the dialog
+                this.modal.show();
+
+                var navigationKeys = [$.ui.keyCode.UP, $.ui.keyCode.PAGE_UP, $.ui.keyCode.DOWN, $.ui.keyCode.PAGE_DOWN];
+
+                // setup the filter
+                $('#processor-type-filter').off('keyup').on('keyup', function (e) {
+                    var code = e.keyCode ? e.keyCode : e.which;
+
+                    // ignore navigation keys
+                    if ($.inArray(code, navigationKeys) !== -1) {
+                        return;
+                    }
+
+                    if (code === $.ui.keyCode.ENTER) {
+                        var selected = grid.getSelectedRows();
+
+                        if (selected.length > 0) {
+                            // grid configured with multi-select = false
+                            var item = grid.getDataItem(selected[0]);
+                            if (isSelectable(item)) {
+                                addProcessor();
+                            }
+                        }
+                    } else {
+                        applyFilter();
+                    }
+                });
+
+                // setup row navigation
+                nfFilteredDialogCommon.addKeydownListener('#processor-type-filter', grid, dataview);
+
+                // adjust the grid canvas now that its been rendered
+                grid.resizeCanvas();
+
+                // auto select the first row if possible
+                if (dataview.getLength() > 0) {
+                    nfFilteredDialogCommon.choseFirstRow(grid);
+                }
+
+                // set the initial focus
+                $('#processor-type-filter').focus()
+            }
+        };
+
+        var processorComponent = new ProcessorComponent();
+        return processorComponent;
+    };
+}));
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js
new file mode 100644 (file)
index 0000000..0cdb1a1
--- /dev/null
@@ -0,0 +1,1587 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* global define, module, require, exports */
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'd3',
+                'nf.ErrorHandler',
+                'nf.Common',
+                'nf.Dialog',
+                'nf.Storage',
+                'nf.Client',
+                'nf.CanvasUtils',
+                'nf.Connection'],
+            function ($, d3, nfErrorHandler, nfCommon, nfDialog, nfStorage, nfClient, nfCanvasUtils, nfConnection) {
+                return (nf.ConnectionConfiguration = factory($, d3, nfErrorHandler, nfCommon, nfDialog, nfStorage, nfClient, nfCanvasUtils, nfConnection));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.ConnectionConfiguration =
+            factory(require('jquery'),
+                require('d3'),
+                require('nf.ErrorHandler'),
+                require('nf.Common'),
+                require('nf.Dialog'),
+                require('nf.Storage'),
+                require('nf.Client'),
+                require('nf.CanvasUtils'),
+                require('nf.Connection')));
+    } else {
+        nf.ConnectionConfiguration = factory(root.$,
+            root.d3,
+            root.nf.ErrorHandler,
+            root.nf.Common,
+            root.nf.Dialog,
+            root.nf.Storage,
+            root.nf.Client,
+            root.nf.CanvasUtils,
+            root.nf.Connection);
+    }
+}(this, function ($, d3, nfErrorHandler, nfCommon, nfDialog, nfStorage, nfClient, nfCanvasUtils, nfConnection) {
+    'use strict';
+
+    var nfBirdseye;
+    var nfGraph;
+
+    var defaultBackPressureObjectThreshold;
+    var defaultBackPressureDataSizeThreshold;
+
+    var CONNECTION_OFFSET_Y_INCREMENT = 75;
+    var CONNECTION_OFFSET_X_INCREMENT = 200;
+
+    var config = {
+        urls: {
+            api: '../nifi-api',
+            prioritizers: '../nifi-api/flow/prioritizers'
+        }
+    };
+
+    /**
+     * Removes the temporary if necessary.
+     */
+    var removeTempEdge = function () {
+        d3.select('path.connector').remove();
+    };
+
+    /**
+     * Activates dialog's button model refresh on a connection relationships change.
+     */
+    var addDialogRelationshipsChangeListener = function() {
+        // refresh button model when a relationship selection changes
+        $('div.available-relationship').bind('change', function() {
+            $('#connection-configuration').modal('refreshButtons');
+        });
+    }
+
+    /**
+     * Initializes the source in the new connection dialog.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceNewConnectionDialog = function (source) {
+        // handle the selected source
+        if (nfCanvasUtils.isProcessor(source)) {
+            return $.Deferred(function (deferred) {
+                // initialize the source processor
+                initializeSourceProcessor(source).done(function (processor) {
+                    if (!nfCommon.isEmpty(processor.relationships)) {
+                        // populate the available connections
+                        $.each(processor.relationships, function (i, relationship) {
+                            createRelationshipOption(relationship.name);
+                        });
+
+                        // resolve the deferred
+                        deferred.resolve();
+                    } else {
+                        // there are no relationships for this processor
+                        nfDialog.showOkDialog({
+                            headerText: 'Connection Configuration',
+                            dialogContent: '\'' + nfCommon.escapeHtml(processor.name) + '\' does not support any relationships.'
+                        });
+
+                        // reset the dialog
+                        resetDialog();
+
+                        deferred.reject();
+                    }
+                }).fail(function () {
+                    deferred.reject();
+                });
+            }).promise();
+        } else {
+            return $.Deferred(function (deferred) {
+                // determine how to initialize the source
+                var connectionSourceDeferred;
+                if (nfCanvasUtils.isInputPort(source)) {
+                    connectionSourceDeferred = initializeSourceInputPort(source);
+                } else if (nfCanvasUtils.isRemoteProcessGroup(source)) {
+                    connectionSourceDeferred = initializeSourceRemoteProcessGroup(source);
+                } else if (nfCanvasUtils.isProcessGroup(source)) {
+                    connectionSourceDeferred = initializeSourceProcessGroup(source);
+                } else {
+                    connectionSourceDeferred = initializeSourceFunnel(source);
+                }
+
+                // finish initialization when appropriate
+                connectionSourceDeferred.done(function () {
+                    deferred.resolve();
+                }).fail(function () {
+                    deferred.reject();
+                });
+            }).promise();
+        }
+    };
+
+    /**
+     * Initializes the source when the source is an input port.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceInputPort = function (source) {
+        return $.Deferred(function (deferred) {
+            // get the input port data
+            var inputPortData = source.datum();
+            var inputPortName = inputPortData.permissions.canRead ? inputPortData.component.name : inputPortData.id;
+
+            // populate the port information
+            $('#input-port-source').show();
+            $('#input-port-source-name').text(inputPortName).attr('title', inputPortName);
+
+            // populate the connection source details
+            $('#connection-source-id').val(inputPortData.id);
+            $('#connection-source-component-id').val(inputPortData.id);
+
+            // populate the group details
+            $('#connection-source-group-id').val(nfCanvasUtils.getGroupId());
+            $('#connection-source-group-name').text(nfCanvasUtils.getGroupName());
+
+            // resolve the deferred
+            deferred.resolve();
+        }).promise();
+    };
+
+    /**
+     * Initializes the source when the source is an input port.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceFunnel = function (source) {
+        return $.Deferred(function (deferred) {
+            // get the funnel data
+            var funnelData = source.datum();
+
+            // populate the port information
+            $('#funnel-source').show();
+
+            // populate the connection source details
+            $('#connection-source-id').val(funnelData.id);
+            $('#connection-source-component-id').val(funnelData.id);
+
+            // populate the group details
+            $('#connection-source-group-id').val(nfCanvasUtils.getGroupId());
+            $('#connection-source-group-name').text(nfCanvasUtils.getGroupName());
+
+            // resolve the deferred
+            deferred.resolve();
+        }).promise();
+    };
+
+    /**
+     * Initializes the source when the source is a processor.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceProcessor = function (source) {
+        return $.Deferred(function (deferred) {
+            // get the processor data
+            var processorData = source.datum();
+            var processorName = processorData.permissions.canRead ? processorData.component.name : processorData.id;
+            var processorType = processorData.permissions.canRead ? nfCommon.substringAfterLast(processorData.component.type, '.') : 'Processor';
+
+            // populate the source processor information
+            $('#processor-source').show();
+            $('#processor-source-name').text(processorName).attr('title', processorName);
+            $('#processor-source-type').text(processorType).attr('title', processorType);
+
+            // populate the connection source details
+            $('#connection-source-id').val(processorData.id);
+            $('#connection-source-component-id').val(processorData.id);
+
+            // populate the group details
+            $('#connection-source-group-id').val(nfCanvasUtils.getGroupId());
+            $('#connection-source-group-name').text(nfCanvasUtils.getGroupName());
+
+            // show the available relationships
+            $('#relationship-names-container').show();
+
+            deferred.resolve(processorData.component);
+        });
+    };
+
+    /**
+     * Initializes the source when the source is a process group.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceProcessGroup = function (source) {
+        return $.Deferred(function (deferred) {
+            // get the process group data
+            var processGroupData = source.datum();
+
+            $.ajax({
+                type: 'GET',
+                url: config.urls.api + '/flow/process-groups/' + encodeURIComponent(processGroupData.id),
+                dataType: 'json'
+            }).done(function (response) {
+                var processGroup = response.processGroupFlow;
+                var processGroupName = response.permissions.canRead ? processGroup.breadcrumb.breadcrumb.name : processGroup.id;
+                var processGroupContents = processGroup.flow;
+
+                // show the output port options
+                var options = [];
+                $.each(processGroupContents.outputPorts, function (i, outputPort) {
+                    // require explicit access to the output port as it's the source of the connection
+                    if (outputPort.permissions.canRead && outputPort.permissions.canWrite) {
+                        var component = outputPort.component;
+                        options.push({
+                            text: component.name,
+                            value: component.id,
+                            description: nfCommon.escapeHtml(component.comments)
+                        });
+                    }
+                });
+
+                // only proceed if there are output ports
+                if (!nfCommon.isEmpty(options)) {
+                    $('#output-port-source').show();
+
+                    // sort the options
+                    options.sort(function (a, b) {
+                        return a.text.localeCompare(b.text);
+                    });
+
+                    // create the combo
+                    $('#output-port-options').combo({
+                        options: options,
+                        maxHeight: 300,
+                        select: function (option) {
+                            $('#connection-source-id').val(option.value);
+                        }
+                    });
+
+                    // populate the connection details
+                    $('#connection-source-component-id').val(processGroup.id);
+
+                    // populate the group details
+                    $('#connection-source-group-id').val(processGroup.id);
+                    $('#connection-source-group-name').text(processGroupName);
+
+                    deferred.resolve();
+                } else {
+                    var message = '\'' + nfCommon.escapeHtml(processGroupName) + '\' does not have any output ports.';
+                    if (nfCommon.isEmpty(processGroupContents.outputPorts) === false) {
+                        message = 'Not authorized for any output ports in \'' + nfCommon.escapeHtml(processGroupName) + '\'.';
+                    }
+
+                    // there are no output ports for this process group
+                    nfDialog.showOkDialog({
+                        headerText: 'Connection Configuration',
+                        dialogContent: message
+                    });
+
+                    // reset the dialog
+                    resetDialog();
+
+                    deferred.reject();
+                }
+            }).fail(function (xhr, status, error) {
+                // handle the error
+                nfErrorHandler.handleAjaxError(xhr, status, error);
+
+                deferred.reject();
+            });
+        }).promise();
+    };
+
+    /**
+     * Initializes the source when the source is a remote process group.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceRemoteProcessGroup = function (source) {
+        return $.Deferred(function (deferred) {
+            // get the remote process group data
+            var remoteProcessGroupData = source.datum();
+
+            $.ajax({
+                type: 'GET',
+                url: remoteProcessGroupData.uri,
+                dataType: 'json'
+            }).done(function (response) {
+                var remoteProcessGroup = response.component;
+                var remoteProcessGroupContents = remoteProcessGroup.contents;
+
+                // only proceed if there are output ports
+                if (!nfCommon.isEmpty(remoteProcessGroupContents.outputPorts)) {
+                    $('#output-port-source').show();
+
+                    // show the output port options
+                    var options = [];
+                    $.each(remoteProcessGroupContents.outputPorts, function (i, outputPort) {
+                        options.push({
+                            text: outputPort.name,
+                            value: outputPort.id,
+                            disabled: outputPort.exists === false,
+                            description: nfCommon.escapeHtml(outputPort.comments)
+                        });
+                    });
+
+                    // sort the options
+                    options.sort(function (a, b) {
+                        return a.text.localeCompare(b.text);
+                    });
+
+                    // create the combo
+                    $('#output-port-options').combo({
+                        options: options,
+                        maxHeight: 300,
+                        select: function (option) {
+                            $('#connection-source-id').val(option.value);
+                        }
+                    });
+
+                    // populate the connection details
+                    $('#connection-source-component-id').val(remoteProcessGroup.id);
+
+                    // populate the group details
+                    $('#connection-source-group-id').val(remoteProcessGroup.id);
+                    $('#connection-source-group-name').text(remoteProcessGroup.name);
+
+                    deferred.resolve();
+                } else {
+                    // there are no relationships for this processor
+                    nfDialog.showOkDialog({
+                        headerText: 'Connection Configuration',
+                        dialogContent: '\'' + nfCommon.escapeHtml(remoteProcessGroup.name) + '\' does not have any output ports.'
+                    });
+
+                    // reset the dialog
+                    resetDialog();
+
+                    deferred.reject();
+                }
+            }).fail(function (xhr, status, error) {
+                // handle the error
+                nfErrorHandler.handleAjaxError(xhr, status, error);
+
+                deferred.reject();
+            });
+        }).promise();
+    };
+
+    var initializeDestinationNewConnectionDialog = function (destination) {
+        if (nfCanvasUtils.isOutputPort(destination)) {
+            return initializeDestinationOutputPort(destination);
+        } else if (nfCanvasUtils.isProcessor(destination)) {
+            return $.Deferred(function (deferred) {
+              initializeDestinationProcessor(destination).done(function (processor) {
+                // Need to add the destination relationships because we need to
+                // provide this to wire up the publishers and subscribers correctly
+                // for a given connection since processors can have multiple
+                // relationships
+                $.each(processor.relationships, function (i, relationship) {
+                    createRelationshipOption(relationship.name);
+                });
+
+                deferred.resolve();
+              }).fail(function () {
+                deferred.reject();
+              });
+            }).promise();
+        } else if (nfCanvasUtils.isRemoteProcessGroup(destination)) {
+            return initializeDestinationRemoteProcessGroup(destination);
+        } else if (nfCanvasUtils.isFunnel(destination)) {
+            return initializeDestinationFunnel(destination);
+        } else {
+            return initializeDestinationProcessGroup(destination);
+        }
+    };
+
+    var initializeDestinationOutputPort = function (destination) {
+        return $.Deferred(function (deferred) {
+            var outputPortData = destination.datum();
+            var outputPortName = outputPortData.permissions.canRead ? outputPortData.component.name : outputPortData.id;
+
+            $('#output-port-destination').show();
+            $('#output-port-destination-name').text(outputPortName).attr('title', outputPortName);
+
+            // populate the connection destination details
+            $('#connection-destination-id').val(outputPortData.id);
+            $('#connection-destination-component-id').val(outputPortData.id);
+
+            // populate the group details
+            $('#connection-destination-group-id').val(nfCanvasUtils.getGroupId());
+            $('#connection-destination-group-name').text(nfCanvasUtils.getGroupName());
+
+            deferred.resolve();
+        }).promise();
+    };
+
+    var initializeDestinationFunnel = function (destination) {
+        return $.Deferred(function (deferred) {
+            var funnelData = destination.datum();
+
+            $('#funnel-destination').show();
+
+            // populate the connection destination details
+            $('#connection-destination-id').val(funnelData.id);
+            $('#connection-destination-component-id').val(funnelData.id);
+
+            // populate the group details
+            $('#connection-destination-group-id').val(nfCanvasUtils.getGroupId());
+            $('#connection-destination-group-name').text(nfCanvasUtils.getGroupName());
+
+            deferred.resolve();
+        }).promise();
+    };
+
+    var initializeDestinationProcessor = function (destination) {
+        return $.Deferred(function (deferred) {
+            var processorData = destination.datum();
+            var processorName = processorData.permissions.canRead ? processorData.component.name : processorData.id;
+            var processorType = processorData.permissions.canRead ? nfCommon.substringAfterLast(processorData.component.type, '.') : 'Processor';
+
+            $('#processor-destination').show();
+            $('#processor-destination-name').text(processorName).attr('title', processorName);
+            $('#processor-destination-type').text(processorType).attr('title', processorType);
+
+            // populate the connection destination details
+            $('#connection-destination-id').val(processorData.id);
+            $('#connection-destination-component-id').val(processorData.id);
+
+            // populate the group details
+            $('#connection-destination-group-id').val(nfCanvasUtils.getGroupId());
+            $('#connection-destination-group-name').text(nfCanvasUtils.getGroupName());
+
+            deferred.resolve(processorData.component);
+        }).promise();
+    };
+
+    /**
+     * Initializes the destination when the destination is a process group.
+     *
+     * @argument {selection} destination        The destination
+     */
+    var initializeDestinationProcessGroup = function (destination) {
+        return $.Deferred(function (deferred) {
+            var processGroupData = destination.datum();
+
+            $.ajax({
+                type: 'GET',
+                url: config.urls.api + '/flow/process-groups/' + encodeURIComponent(processGroupData.id),
+                dataType: 'json'
+            }).done(function (response) {
+                var processGroup = response.processGroupFlow;
+                var processGroupName = response.permissions.canRead ? processGroup.breadcrumb.breadcrumb.name : processGroup.id;
+                var processGroupContents = processGroup.flow;
+
+                // show the input port options
+                var options = [];
+                $.each(processGroupContents.inputPorts, function (i, inputPort) {
+                    options.push({
+                        text: inputPort.permissions.canRead ? inputPort.component.name : inputPort.id,
+                        value: inputPort.id,
+                        description: inputPort.permissions.canRead ? nfCommon.escapeHtml(inputPort.component.comments) : null
+                    });
+                });
+
+                // only proceed if there are output ports
+                if (!nfCommon.isEmpty(options)) {
+                    $('#input-port-destination').show();
+
+                    // sort the options
+                    options.sort(function (a, b) {
+                        return a.text.localeCompare(b.text);
+                    });
+
+                    // create the combo
+                    $('#input-port-options').combo({
+                        options: options,
+                        maxHeight: 300,
+                        select: function (option) {
+                            $('#connection-destination-id').val(option.value);
+                        }
+                    });
+
+                    // populate the connection details
+                    $('#connection-destination-component-id').val(processGroup.id);
+
+                    // populate the group details
+                    $('#connection-destination-group-id').val(processGroup.id);
+                    $('#connection-destination-group-name').text(processGroupName);
+
+                    deferred.resolve();
+                } else {
+                    // there are no relationships for this processor
+                    nfDialog.showOkDialog({
+                        headerText: 'Connection Configuration',
+                        dialogContent: '\'' + nfCommon.escapeHtml(processGroupName) + '\' does not have any input ports.'
+                    });
+
+                    // reset the dialog
+                    resetDialog();
+
+                    deferred.reject();
+                }
+            }).fail(function (xhr, status, error) {
+                // handle the error
+                nfErrorHandler.handleAjaxError(xhr, status, error);
+
+                deferred.reject();
+            });
+        }).promise();
+    };
+
+    /**
+     * Initializes the source when the source is a remote process group.
+     *
+     * @argument {selection} destination        The destination
+     * @argument {object} connectionDestination The connection destination object
+     */
+    var initializeDestinationRemoteProcessGroup = function (destination, connectionDestination) {
+        return $.Deferred(function (deferred) {
+            var remoteProcessGroupData = destination.datum();
+
+            $.ajax({
+                type: 'GET',
+                url: remoteProcessGroupData.uri,
+                dataType: 'json'
+            }).done(function (response) {
+                var remoteProcessGroup = response.component;
+                var remoteProcessGroupContents = remoteProcessGroup.contents;
+
+                // only proceed if there are output ports
+                if (!nfCommon.isEmpty(remoteProcessGroupContents.inputPorts)) {
+                    $('#input-port-destination').show();
+
+                    // show the input port options
+                    var options = [];
+                    $.each(remoteProcessGroupContents.inputPorts, function (i, inputPort) {
+                        options.push({
+                            text: inputPort.name,
+                            value: inputPort.id,
+                            disabled: inputPort.exists === false,
+                            description: nfCommon.escapeHtml(inputPort.comments)
+                        });
+                    });
+
+                    // sort the options
+                    options.sort(function (a, b) {
+                        return a.text.localeCompare(b.text);
+                    });
+
+                    // create the combo
+                    $('#input-port-options').combo({
+                        options: options,
+                        maxHeight: 300,
+                        select: function (option) {
+                            $('#connection-destination-id').val(option.value);
+                        }
+                    });
+
+                    // populate the connection details
+                    $('#connection-destination-component-id').val(remoteProcessGroup.id);
+
+                    // populate the group details
+                    $('#connection-destination-group-id').val(remoteProcessGroup.id);
+                    $('#connection-destination-group-name').text(remoteProcessGroup.name);
+
+                    deferred.resolve();
+                } else {
+                    // there are no relationships for this processor
+                    nfDialog.showOkDialog({
+                        headerText: 'Connection Configuration',
+                        dialogContent: '\'' + nfCommon.escapeHtml(remoteProcessGroup.name) + '\' does not have any input ports.'
+                    });
+
+                    // reset the dialog
+                    resetDialog();
+
+                    deferred.reject();
+                }
+            }).fail(function (xhr, status, error) {
+                // handle the error
+                nfErrorHandler.handleAjaxError(xhr, status, error);
+
+                deferred.reject();
+            });
+        }).promise();
+    };
+
+    /**
+     * Initializes the source panel for groups.
+     *
+     * @argument {selection} source    The source of the connection
+     */
+    var initializeSourceReadOnlyGroup = function (source) {
+        return $.Deferred(function (deferred) {
+            var sourceData = source.datum();
+            var sourceName = sourceData.permissions.canRead ? sourceData.component.name : sourceData.id;
+
+            // populate the port information
+            $('#read-only-output-port-source').show();
+
+            // populate the component information
+            $('#connection-source-component-id').val(sourceData.id);
+
+            // populate the group details
+            $('#connection-source-group-id').val(sourceData.id);
+            $('#connection-source-group-name').text(sourceName);
+
+            // resolve the deferred
+            deferred.resolve();
+        }).promise();
+    };
+
+    /**
+     * Initializes the source in the existing connection dialog.
+     *
+     * @argument {selection} source        The source
+     */
+    var initializeSourceEditConnectionDialog = function (source) {
+        if (nfCanvasUtils.isProcessor(source)) {
+            return initializeSourceProcessor(source);
+        } else if (nfCanvasUtils.isInputPort(source)) {
+            return initializeSourceInputPort(source);
+        } else if (nfCanvasUtils.isFunnel(source)) {
+            return initializeSourceFunnel(source);
+        } else {
+            return initializeSourceReadOnlyGroup(source);
+        }
+    };
+
+    /**
+     * Initializes the destination in the existing connection dialog.
+     *
+     * @argument {selection} destination        The destination
+     * @argument {object} connectionDestination The connection destination object
+     */
+    var initializeDestinationEditConnectionDialog = function (destination, connectionDestination) {
+        if (nfCanvasUtils.isProcessor(destination)) {
+            return initializeDestinationProcessor(destination);
+        } else if (nfCanvasUtils.isOutputPort(destination)) {
+            return initializeDestinationOutputPort(destination);
+        } else if (nfCanvasUtils.isRemoteProcessGroup(destination)) {
+            return initializeDestinationRemoteProcessGroup(destination, connectionDestination);
+        } else if (nfCanvasUtils.isFunnel(destination)) {
+            return initializeDestinationFunnel(destination);
+        } else {
+            return initializeDestinationProcessGroup(destination);
+        }
+    };
+
+    /**
+     * Creates an option for the specified relationship name.
+     *
+     * @argument {string} name      The relationship name
+     */
+    var createRelationshipOption = function (name) {
+        var nameSplit = name.split(":");
+        var nameLabel = name;
+
+        if (nameSplit.length > 1) {
+            // Example: publishes:data_transformation_format:1.0.0:message_router:stream_publish_url
+            var pubSub = nameSplit[0];
+            pubSub = pubSub.charAt(0).toUpperCase() + pubSub.slice(1);
+            nameLabel = pubSub + " " + nameSplit[1] + "/" + nameSplit[2] + " on " + nameSplit[4];
+        }
+
+        var relationshipLabel = $('<div class="relationship-name nf-checkbox-label ellipsis"></div>').text(nameLabel);
+        var relationshipValue = $('<span class="relationship-name-value hidden"></span>').text(name);
+        return $('<div class="available-relationship-container"><div class="available-relationship nf-checkbox checkbox-unchecked"></div>' +
+            '</div>').append(relationshipLabel).append(relationshipValue).appendTo('#relationship-names');
+    };
+
+    /**
+     * Adds a new connection.
+     *
+     * @argument {array} selectedRelationships      The selected relationships
+     */
+    var addConnection = function (selectedRelationships) {
+        // get the connection details
+        var sourceId = $('#connection-source-id').val();
+        var destinationId = $('#connection-destination-id').val();
+
+        // get the selection components
+        var sourceComponentId = $('#connection-source-component-id').val();
+        var source = d3.select('#id-' + sourceComponentId);
+        var destinationComponentId = $('#connection-destination-component-id').val();
+        var destination = d3.select('#id-' + destinationComponentId);
+
+        // get the source/destination data
+        var sourceData = source.datum();
+        var destinationData = destination.datum();
+
+        // add bend points if we're dealing with a self loop
+        var bends = [];
+        if (sourceComponentId === destinationComponentId) {
+            var rightCenter = {
+                x: sourceData.position.x + (sourceData.dimensions.width),
+                y: sourceData.position.y + (sourceData.dimensions.height / 2)
+            };
+
+            var xOffset = nfConnection.config.selfLoopXOffset;
+            var yOffset = nfConnection.config.selfLoopYOffset;
+            bends.push({
+                'x': (rightCenter.x + xOffset),
+                'y': (rightCenter.y - yOffset)
+            });
+            bends.push({
+                'x': (rightCenter.x + xOffset),
+                'y': (rightCenter.y + yOffset)
+            });
+        } else {
+            var existingConnections = [];
+
+            // get all connections for the source component
+            var connectionsForSourceComponent = nfConnection.getComponentConnections(sourceComponentId);
+            $.each(connectionsForSourceComponent, function (_, connectionForSourceComponent) {
+                // get the id for the source/destination component
+                var connectionSourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(connectionForSourceComponent);
+                var connectionDestinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(connectionForSourceComponent);
+
+                // if the connection is between these same components, consider it for collisions
+                if ((connectionSourceComponentId === sourceComponentId && connectionDestinationComponentId === destinationComponentId) ||
+                    (connectionDestinationComponentId === sourceComponentId && connectionSourceComponentId === destinationComponentId)) {
+
+                    // record all connections between these two components in question
+                    existingConnections.push(connectionForSourceComponent);
+                }
+            });
+
+            // if there are existing connections between these components, ensure the new connection won't collide
+            if (existingConnections.length > 0) {
+                var avoidCollision = false;
+                $.each(existingConnections, function (_, existingConnection) {
+                    // only consider multiple connections with no bend points a collision, the existance of 
+                    // bend points suggests that the user has placed the connection into a desired location
+                    if (nfCommon.isEmpty(existingConnection.bends)) {
+                        avoidCollision = true;
+                        return false;
+                    }
+                });
+
+                // if we need to avoid a collision
+                if (avoidCollision === true) {
+                    // determine the middle of the source/destination components
+                    var sourceMiddle = [sourceData.position.x + (sourceData.dimensions.width / 2), sourceData.position.y + (sourceData.dimensions.height / 2)];
+                    var destinationMiddle = [destinationData.position.x + (destinationData.dimensions.width / 2), destinationData.position.y + (destinationData.dimensions.height / 2)];
+
+                    // detect if the line is more horizontal or vertical
+                    var slope = ((sourceMiddle[1] - destinationMiddle[1]) / (sourceMiddle[0] - destinationMiddle[0]));
+                    var isMoreHorizontal = slope <= 1 && slope >= -1;
+
+                    // determines if the specified coordinate collides with another connection
+                    var collides = function (x, y) {
+                        var collides = false;
+                        $.each(existingConnections, function (_, existingConnection) {
+                            if (!nfCommon.isEmpty(existingConnection.bends)) {
+                                if (isMoreHorizontal) {
+                                    // horizontal lines are adjusted in the y space
+                                    if (existingConnection.bends[0].y === y) {
+                                        collides = true;
+                                        return false;
+                                    }
+                                } else {
+                                    // vertical lines are adjusted in the x space
+                                    if (existingConnection.bends[0].x === x) {
+                                        collides = true;
+                                        return false;
+                                    }
+                                }
+                            }
+                        });
+                        return collides;
+                    };
+
+                    // find the mid point on the connection
+                    var xCandidate = (sourceMiddle[0] + destinationMiddle[0]) / 2;
+                    var yCandidate = (sourceMiddle[1] + destinationMiddle[1]) / 2;
+
+                    // attempt to position this connection so it doesn't collide
+                    var xStep = isMoreHorizontal ? 0 : CONNECTION_OFFSET_X_INCREMENT;
+                    var yStep = isMoreHorizontal ? CONNECTION_OFFSET_Y_INCREMENT : 0;
+                    var positioned = false;
+                    while (positioned === false) {
+                        // consider above and below, then increment and try again (if necessary)
+                        if (collides(xCandidate - xStep, yCandidate - yStep) === false) {
+                            bends.push({
+                                'x': (xCandidate - xStep),
+                                'y': (yCandidate - yStep)
+                            });
+                            positioned = true;
+                        } else if (collides(xCandidate + xStep, yCandidate + yStep) === false) {
+                            bends.push({
+                                'x': (xCandidate + xStep),
+                                'y': (yCandidate + yStep)
+                            });
+                            positioned = true;
+                        }
+
+                        if (isMoreHorizontal) {
+                            yStep += CONNECTION_OFFSET_Y_INCREMENT;
+                        } else {
+                            xStep += CONNECTION_OFFSET_X_INCREMENT;
+                        }
+                    }
+                }
+            }
+        }
+
+        // determine the source group id
+        var sourceGroupId = $('#connection-source-group-id').val();
+        var destinationGroupId = $('#connection-destination-group-id').val();
+
+        // determine the source and destination types
+        var sourceType = nfCanvasUtils.getConnectableTypeForSource(source);
+        var destinationType = nfCanvasUtils.getConnectableTypeForDestination(destination);
+
+        // get the settings
+        var connectionName = $('#connection-name').val();
+        var flowFileExpiration = $('#flow-file-expiration').val();
+        var backPressureObjectThreshold = $('#back-pressure-object-threshold').val();
+        var backPressureDataSizeThreshold = $('#back-pressure-data-size-threshold').val();
+        var prioritizers = $('#prioritizer-selected').sortable('toArray');
+        var loadBalanceStrategy = $('#load-balance-strategy-combo').combo('getSelectedOption').value;
+        var shouldLoadBalance = 'DO_NOT_LOAD_BALANCE' !== loadBalanceStrategy;
+        var loadBalancePartitionAttribute = shouldLoadBalance && 'PARTITION_BY_ATTRIBUTE' === loadBalanceStrategy ? $('#load-balance-partition-attribute').val() : '';
+        var loadBalanceCompression = shouldLoadBalance ? $('#load-balance-compression-combo').combo('getSelectedOption').value : 'DO_NOT_COMPRESS';
+
+        if (validateSettings()) {
+            var connectionEntity = {
+                'revision': nfClient.getRevision({
+                    'revision': {
+                        'version': 0
+                    }
+                }),
+                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+                'component': {
+                    'name': connectionName,
+                    'source': {
+                        'id': sourceId,
+                        'groupId': sourceGroupId,
+                        'type': sourceType
+                    },
+                    'destination': {
+                        'id': destinationId,
+                        'groupId': destinationGroupId,
+                        'type': destinationType
+                    },
+                    'selectedRelationships': selectedRelationships,
+                    'flowFileExpiration': flowFileExpiration,
+                    'backPressureDataSizeThreshold': backPressureDataSizeThreshold,
+                    'backPressureObjectThreshold': backPressureObjectThreshold,
+                    'bends': bends,
+                    'prioritizers': prioritizers,
+                    'loadBalanceStrategy': loadBalanceStrategy,
+                    'loadBalancePartitionAttribute': loadBalancePartitionAttribute,
+                    'loadBalanceCompression': loadBalanceCompression
+                }
+            };
+
+            // create the new connection
+            $.ajax({
+                type: 'POST',
+                url: config.urls.api + '/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/connections',
+                data: JSON.stringify(connectionEntity),
+                dataType: 'json',
+                contentType: 'application/json'
+            }).done(function (response) {
+                // add the connection
+                nfGraph.add({
+                    'connections': [response]
+                }, {
+                    'selectAll': true
+                });
+
+                // reload the connections source/destination components
+                nfCanvasUtils.reloadConnectionSourceAndDestination(sourceComponentId, destinationComponentId);
+
+                // update component visibility
+                nfGraph.updateVisibility();
+
+                // update the birdseye
+                nfBirdseye.refresh();
+            }).fail(function (xhr, status, error) {
+                // handle the error
+                nfErrorHandler.handleAjaxError(xhr, status, error);
+            });
+        }
+    };
+
+    /**
+     * Updates an existing connection.
+     *
+     * @argument {array} selectedRelationships          The selected relationships
+     */
+    var updateConnection = function (selectedRelationships) {
+        // get the connection details
+        var connectionId = $('#connection-id').text();
+        var connectionUri = $('#connection-uri').val();
+
+        // get the source details
+        var sourceComponentId = $('#connection-source-component-id').val();
+
+        // get the destination details
+        var destinationComponentId = $('#connection-destination-component-id').val();
+        var destination = d3.select('#id-' + destinationComponentId);
+        var destinationType = nfCanvasUtils.getConnectableTypeForDestination(destination);
+
+        // get the destination details
+        var destinationId = $('#connection-destination-id').val();
+        var destinationGroupId = $('#connection-destination-group-id').val();
+
+        // get the settings
+        var connectionName = $('#connection-name').val();
+        var flowFileExpiration = $('#flow-file-expiration').val();
+        var backPressureObjectThreshold = $('#back-pressure-object-threshold').val();
+        var backPressureDataSizeThreshold = $('#back-pressure-data-size-threshold').val();
+        var prioritizers = $('#prioritizer-selected').sortable('toArray');
+        var loadBalanceStrategy = $('#load-balance-strategy-combo').combo('getSelectedOption').value;
+        var shouldLoadBalance = 'DO_NOT_LOAD_BALANCE' !== loadBalanceStrategy;
+        var loadBalancePartitionAttribute = shouldLoadBalance && 'PARTITION_BY_ATTRIBUTE' === loadBalanceStrategy ? $('#load-balance-partition-attribute').val() : '';
+        var loadBalanceCompression = shouldLoadBalance ? $('#load-balance-compression-combo').combo('getSelectedOption').value : 'DO_NOT_COMPRESS';
+
+        if (validateSettings()) {
+            var d = nfConnection.get(connectionId);
+            var connectionEntity = {
+                'revision': nfClient.getRevision(d),
+                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+                'component': {
+                    'id': connectionId,
+                    'name': connectionName,
+                    'destination': {
+                        'id': destinationId,
+                        'groupId': destinationGroupId,
+                        'type': destinationType
+                    },
+                    'selectedRelationships': selectedRelationships,
+                    'flowFileExpiration': flowFileExpiration,
+                    'backPressureDataSizeThreshold': backPressureDataSizeThreshold,
+                    'backPressureObjectThreshold': backPressureObjectThreshold,
+                    'prioritizers': prioritizers,
+                    'loadBalanceStrategy': loadBalanceStrategy,
+                    'loadBalancePartitionAttribute': loadBalancePartitionAttribute,
+                    'loadBalanceCompression': loadBalanceCompression
+                }
+            };
+
+            // update the connection
+            return $.ajax({
+                type: 'PUT',
+                url: connectionUri,
+                data: JSON.stringify(connectionEntity),
+                dataType: 'json',
+                contentType: 'application/json'
+            }).done(function (response) {
+                // update this connection
+                nfConnection.set(response);
+
+                // reload the connections source/destination components
+                nfCanvasUtils.reloadConnectionSourceAndDestination(sourceComponentId, destinationComponentId);
+            }).fail(function (xhr, status, error) {
+                if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) {
+                    nfDialog.showOkDialog({
+                        headerText: 'Connection Configuration',
+                        dialogContent: nfCommon.escapeHtml(xhr.responseText),
+                    });
+                } else {
+                    nfErrorHandler.handleAjaxError(xhr, status, error);
+                }
+            });
+        } else {
+            return $.Deferred(function (deferred) {
+                deferred.reject();
+            }).promise();
+        }
+    };
+
+    /**
+     * Returns an array of selected relationship names.
+     */
+    var getSelectedRelationships = function () {
+        // get all available relationships
+        var availableRelationships = $('#relationship-names');
+        var selectedRelationships = [];
+
+        // go through each relationship to determine which are selected
+        $.each(availableRelationships.children(), function (i, relationshipElement) {
+            var relationship = $(relationshipElement);
+
+            // get each relationship and its corresponding checkbox
+            var relationshipCheck = relationship.children('div.available-relationship');
+
+            // see if this relationship has been selected
+            if (relationshipCheck.hasClass('checkbox-checked')) {
+                selectedRelationships.push(relationship.children('span.relationship-name-value').text());
+            }
+        });
+
+        return selectedRelationships;
+    };
+
+    /**
+     * Validates the specified settings.
+     */
+    var validateSettings = function () {
+        var errors = [];
+
+        // validate the settings
+        if (nfCommon.isBlank($('#flow-file-expiration').val())) {
+            errors.push('File expiration must be specified');
+        }
+        if (!$.isNumeric($('#back-pressure-object-threshold').val())) {
+            errors.push('Back pressure object threshold must be an integer value');
+        }
+        if (nfCommon.isBlank($('#back-pressure-data-size-threshold').val())) {
+            errors.push('Back pressure data size threshold must be specified');
+        }
+        if ($('#load-balance-strategy-combo').combo('getSelectedOption').value === 'PARTITION_BY_ATTRIBUTE'
+            && nfCommon.isBlank($('#load-balance-partition-attribute').val())) {
+            errors.push('Cannot set Load Balance Strategy to "Partition by attribute" without providing a partitioning "Attribute Name"');
+        }
+
+        if (errors.length > 0) {
+            nfDialog.showOkDialog({
+                headerText: 'Connection Configuration',
+                dialogContent: nfCommon.formatUnorderedList(errors)
+            });
+            return false;
+        } else {
+            return true;
+        }
+    };
+
+    /**
+     * Resets the dialog.
+     */
+    var resetDialog = function () {
+        // reset the prioritizers
+        var selectedList = $('#prioritizer-selected');
+        var availableList = $('#prioritizer-available');
+        selectedList.children().detach().appendTo(availableList);
+
+        // sort the available list
+        var listItems = availableList.children('li').get();
+        listItems.sort(function (a, b) {
+            var compA = $(a).text().toUpperCase();
+            var compB = $(b).text().toUpperCase();
+            return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+        });
+
+        // clear the available list and re-insert each list item
+        $.each(listItems, function () {
+            $(this).detach();
+        });
+        $.each(listItems, function () {
+            $(this).appendTo(availableList);
+        });
+
+        // reset the fields
+        $('#connection-name').val('');
+        $('#relationship-names').css('border-width', '0').empty();
+        $('#relationship-names-container').show();
+
+        // clear the id field
+        nfCommon.clearField('connection-id');
+
+        // hide all the connection source panels
+        $('#processor-source').hide();
+        $('#input-port-source').hide();
+        $('#output-port-source').hide();
+        $('#read-only-output-port-source').hide();
+        $('#funnel-source').hide();
+
+        // hide all the connection destination panels
+        $('#processor-destination').hide();
+        $('#input-port-destination').hide();
+        $('#output-port-destination').hide();
+        $('#funnel-destination').hide();
+
+        // clear and destination details
+        $('#connection-source-id').val('');
+        $('#connection-source-component-id').val('');
+        $('#connection-source-group-id').val('');
+
+        // clear any destination details
+        $('#connection-destination-id').val('');
+        $('#connection-destination-component-id').val('');
+        $('#connection-destination-group-id').val('');
+
+        // clear any ports
+        $('#output-port-options').empty();
+        $('#input-port-options').empty();
+
+        // clear load balance settings
+        $('#load-balance-strategy-combo').combo('setSelectedOption', nfCommon.loadBalanceStrategyOptions[0]);
+        $('#load-balance-partition-attribute').val('');
+        $('#load-balance-compression-combo').combo('setSelectedOption', nfCommon.loadBalanceCompressionOptions[0]);
+
+        // see if the temp edge needs to be removed
+        removeTempEdge();
+    };
+
+    var nfConnectionConfiguration = {
+
+        /**
+         * Initialize the connection configuration.
+         *
+         * @param nfBirdseyeRef   The nfBirdseye module.
+         * @param nfGraphRef   The nfGraph module.
+         */
+        init: function (nfBirdseyeRef, nfGraphRef, defaultBackPressureObjectThresholdRef, defaultBackPressureDataSizeThresholdRef) {
+            nfBirdseye = nfBirdseyeRef;
+            nfGraph = nfGraphRef;
+
+            defaultBackPressureObjectThreshold = defaultBackPressureObjectThresholdRef;
+            defaultBackPressureDataSizeThreshold = defaultBackPressureDataSizeThresholdRef;
+
+            // initially hide the relationship names container
+            $('#relationship-names-container').show();
+
+            // initialize the configure connection dialog
+            $('#connection-configuration').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Configure Connection',
+                handler: {
+                    close: function () {
+                        // reset the dialog on close
+                        resetDialog();
+                    },
+                    open: function () {
+                        nfCommon.toggleScrollable($('#' + this.find('.tab-container').attr('id') + '-content').get(0));
+                    }
+                }
+            });
+
+            // initialize the properties tabs
+            $('#connection-configuration-tabs').tabbs({
+                tabStyle: 'tab',
+                selectedTabStyle: 'selected-tab',
+                scrollableTabContentStyle: 'scrollable',
+                tabs: [{
+                    name: 'Details',
+                    tabContentId: 'connection-details-tab-content'
+                }, {
+                    name: 'Settings',
+                    tabContentId: 'connection-settings-tab-content'
+                }]
+            });
+
+            // initialize the load balance strategy combo
+            $('#load-balance-strategy-combo').combo({
+                options: nfCommon.loadBalanceStrategyOptions,
+                select: function (selectedOption) {
+                    // Show the appropriate configurations
+                    if (selectedOption.value === 'PARTITION_BY_ATTRIBUTE') {
+                        $('#load-balance-partition-attribute-setting-separator').show();
+                        $('#load-balance-partition-attribute-setting').show();
+                    } else {
+                        $('#load-balance-partition-attribute-setting-separator').hide();
+                        $('#load-balance-partition-attribute-setting').hide();
+                    }
+                    if (selectedOption.value === 'DO_NOT_LOAD_BALANCE') {
+                        $('#load-balance-compression-setting').hide();
+                    } else {
+                        $('#load-balance-compression-setting').show();
+                    }
+                }
+            });
+
+
+            // initialize the load balance compression combo
+            $('#load-balance-compression-combo').combo({
+                options: nfCommon.loadBalanceCompressionOptions
+            });
+
+            // load the processor prioritizers
+            $.ajax({
+                type: 'GET',
+                url: config.urls.prioritizers,
+                dataType: 'json'
+            }).done(function (response) {
+                // create an element for each available prioritizer
+                $.each(response.prioritizerTypes, function (i, documentedType) {
+                    nfConnectionConfiguration.addAvailablePrioritizer('#prioritizer-available', documentedType);
+                });
+
+                // make the prioritizer containers sortable
+                $('#prioritizer-available, #prioritizer-selected').sortable({
+                    containment: $('#connection-settings-tab-content').find('.settings-right'),
+                    connectWith: 'ul',
+                    placeholder: 'ui-state-highlight',
+                    scroll: true,
+                    opacity: 0.6
+                });
+                $('#prioritizer-available, #prioritizer-selected').disableSelection();
+            }).fail(nfErrorHandler.handleAjaxError);
+        },
+
+        /**
+         * Adds the specified prioritizer to the specified container.
+         *
+         * @argument {string} prioritizerContainer      The dom Id of the prioritizer container
+         * @argument {object} prioritizerType           The type of prioritizer
+         */
+        addAvailablePrioritizer: function (prioritizerContainer, prioritizerType) {
+            var type = prioritizerType.type;
+            var name = nfCommon.substringAfterLast(type, '.');
+
+            // add the prioritizers to the available list
+            var prioritizerList = $(prioritizerContainer);
+            var prioritizer = $('<li></li>').append($('<span style="float: left;"></span>').text(name)).attr('id', type).addClass('ui-state-default').appendTo(prioritizerList);
+
+            // add the description if applicable
+            if (nfCommon.isDefinedAndNotNull(prioritizerType.description)) {
+                $('<div class="fa fa-question-circle"></div>').appendTo(prioritizer).qtip($.extend({
+                    content: nfCommon.escapeHtml(prioritizerType.description)
+                }, nfCommon.config.tooltipConfig));
+            }
+        },
+
+        /**
+         * Shows the dialog for creating a new connection.
+         *
+         * @argument {string} sourceId      The source id
+         * @argument {string} destinationId The destination id
+         */
+        createConnection: function (sourceId, destinationId) {
+            // select the source and destination
+            var source = d3.select('#id-' + sourceId);
+            var destination = d3.select('#id-' + destinationId);
+
+            if (source.empty() || destination.empty()) {
+                return;
+            }
+
+            // initialize the connection dialog
+            $.when(initializeSourceNewConnectionDialog(source), initializeDestinationNewConnectionDialog(destination)).done(function () {
+
+                if (nfCanvasUtils.isProcessor(source) || nfCanvasUtils.isProcessor(destination)) {
+                    addDialogRelationshipsChangeListener();
+
+                    // if there is a single relationship auto select
+                    var relationships = $('#relationship-names').children('div');
+                    if (relationships.length === 1) {
+                        relationships.children('div.available-relationship').removeClass('checkbox-unchecked').addClass('checkbox-checked');
+                    }
+
+                    // configure the button model
+                    $('#connection-configuration').modal('setButtonModel', [{
+                        buttonText: 'Add',
+                        color: {
+                            base: '#728E9B',
+                            hover: '#004849',
+                            text: '#ffffff'
+                        },
+                        disabled: function () {
+                            // ensure some relationships were selected
+                            return getSelectedRelationships().length === 0;
+                        },
+                        handler: {
+                            click: function () {
+                                addConnection(getSelectedRelationships());
+
+                                // close the dialog
+                                $('#connection-configuration').modal('hide');
+                            }
+                        }
+                    },
+                        {
+                            buttonText: 'Cancel',
+                            color: {
+                                base: '#E3E8EB',
+                                hover: '#C7D2D7',
+                                text: '#004849'
+                            },
+                            handler: {
+                                click: function () {
+                                    $('#connection-configuration').modal('hide');
+                                }
+                            }
+                        }]);
+                } else {
+                    // configure the button model
+                    $('#connection-configuration').modal('setButtonModel', [{
+                        buttonText: 'Add',
+                        color: {
+                            base: '#728E9B',
+                            hover: '#004849',
+                            text: '#ffffff'
+                        },
+                        handler: {
+                            click: function () {
+                                // add the connection
+                                addConnection();
+
+                                // close the dialog
+                                $('#connection-configuration').modal('hide');
+                            }
+                        }
+                    },
+                        {
+                            buttonText: 'Cancel',
+                            color: {
+                                base: '#E3E8EB',
+                                hover: '#C7D2D7',
+                                text: '#004849'
+                            },
+                            handler: {
+                                click: function () {
+                                    $('#connection-configuration').modal('hide');
+                                }
+                            }
+                        }]);
+                }
+
+                // set the default values
+                $('#flow-file-expiration').val('0 sec');
+                $('#back-pressure-object-threshold').val(defaultBackPressureObjectThreshold);
+                $('#back-pressure-data-size-threshold').val(defaultBackPressureDataSizeThreshold);
+
+                // select the first tab
+                $('#connection-configuration-tabs').find('li:first').click();
+
+                // configure the header and show the dialog
+                $('#connection-configuration').modal('setHeaderText', 'Create Connection').modal('show');
+
+                // add the ellipsis if necessary
+                $('#connection-configuration div.relationship-name').ellipsis();
+
+                // fill in the connection id
+                nfCommon.populateField('connection-id', null);
+
+                // show the border if necessary
+                var relationshipNames = $('#relationship-names');
+                if (relationshipNames.is(':visible') && relationshipNames.get(0).scrollHeight > Math.round(relationshipNames.innerHeight())) {
+                    relationshipNames.css('border-width', '1px');
+                }
+            }).fail(function () {
+                // see if the temp edge needs to be removed
+                removeTempEdge();
+            });
+        },
+
+        /**
+         * Shows the configuration for the specified connection. If a destination is
+         * specified it will be considered a new destination.
+         *
+         * @argument {selection} selection         The connection entry
+         * @argument {selection} destination          Optional new destination
+         */
+        showConfiguration: function (selection, destination) {
+            return $.Deferred(function (deferred) {
+                var connectionEntry = selection.datum();
+                var connection = connectionEntry.component;
+
+                // identify the source component
+                var sourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(connectionEntry);
+                var source = d3.select('#id-' + sourceComponentId);
+
+                // identify the destination component
+                if (nfCommon.isUndefinedOrNull(destination)) {
+                    var destinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(connectionEntry);
+                    destination = d3.select('#id-' + destinationComponentId);
+                }
+
+                // initialize the connection dialog
+                $.when(initializeSourceEditConnectionDialog(source), initializeDestinationEditConnectionDialog(destination, connection.destination)).done(function () {
+                    var availableRelationships = connection.availableRelationships;
+                    var selectedRelationships = connection.selectedRelationships;
+
+                    // Added this block to force add destination relationships to
+                    // get blueprint generation working
+                    if (nfCanvasUtils.isProcessor(destination)) {
+                      if (availableRelationships == undefined) {
+                        // When the source is a port, this could be null or
+                        // undefined since the backend the attribute doesn't
+                        // exist
+                        availableRelationships = [];
+                      }
+
+                      var processorData = destination.datum();
+                      $.each(processorData.component.relationships, function (i, relationship) {
+                          availableRelationships.push(relationship.name);
+                      });
+                    }
+
+                    // show the available relationship if applicable
+                    if (nfCommon.isDefinedAndNotNull(availableRelationships) || nfCommon.isDefinedAndNotNull(selectedRelationships)) {
+                        // populate the available connections
+                        $.each(availableRelationships, function (i, name) {
+                            createRelationshipOption(name);
+                        });
+
+                        addDialogRelationshipsChangeListener();
+
+                        // ensure all selected relationships are present
+                        // (may be undefined) and selected
+                        $.each(selectedRelationships, function (i, name) {
+                            // mark undefined relationships accordingly
+                            if ($.inArray(name, availableRelationships) === -1) {
+                                var option = createRelationshipOption(name);
+                                $(option).children('div.relationship-name').addClass('undefined');
+                            }
+
+                            // ensure all selected relationships are checked
+                            var relationships = $('#relationship-names').children('div');
+                            $.each(relationships, function (i, relationship) {
+                                var relationshipName = $(relationship).children('span.relationship-name-value');
+                                if (relationshipName.text() === name) {
+                                    $(relationship).children('div.available-relationship').removeClass('checkbox-unchecked').addClass('checkbox-checked');
+                                }
+                            });
+                        });
+                    }
+
+                    // if the source is a process group or remote process group, select the appropriate port if applicable
+                    if (nfCanvasUtils.isProcessGroup(source) || nfCanvasUtils.isRemoteProcessGroup(source)) {
+                        // populate the connection source details
+                        $('#connection-source-id').val(connection.source.id);
+                        $('#read-only-output-port-name').text(connection.source.name).attr('title', connection.source.name);
+                    }
+
+                    // if the destination is a process gorup or remote process group, select the appropriate port if applicable
+                    if (nfCanvasUtils.isProcessGroup(destination) || nfCanvasUtils.isRemoteProcessGroup(destination)) {
+                        var destinationData = destination.datum();
+
+                        // when the group ids differ, its a new destination component so we don't want to preselect any port
+                        if (connection.destination.groupId === destinationData.id) {
+                            $('#input-port-options').combo('setSelectedOption', {
+                                value: connection.destination.id
+                            });
+                        }
+                    }
+
+                    // set the connection settings
+                    $('#connection-name').val(connection.name);
+                    $('#flow-file-expiration').val(connection.flowFileExpiration);
+                    $('#back-pressure-object-threshold').val(connection.backPressureObjectThreshold);
+                    $('#back-pressure-data-size-threshold').val(connection.backPressureDataSizeThreshold);
+
+                    // select the load balance combos
+                    $('#load-balance-strategy-combo').combo('setSelectedOption', {
+                        value: connection.loadBalanceStrategy
+                    });
+                    $('#load-balance-compression-combo').combo('setSelectedOption', {
+                        value: connection.loadBalanceCompression
+                    });
+                    $('#load-balance-partition-attribute').val(connection.loadBalancePartitionAttribute);
+
+                    // format the connection id
+                    nfCommon.populateField('connection-id', connection.id);
+
+                    // handle each prioritizer
+                    $.each(connection.prioritizers, function (i, type) {
+                        $('#prioritizer-available').children('li[id="' + type + '"]').detach().appendTo('#prioritizer-selected');
+                    });
+
+                    // store the connection details
+                    $('#connection-uri').val(connectionEntry.uri);
+
+                    // configure the button model
+                    $('#connection-configuration').modal('setButtonModel', [{
+                        buttonText: 'Apply',
+                        color: {
+                            base: '#728E9B',
+                            hover: '#004849',
+                            text: '#ffffff'
+                        },
+                        disabled: function () {
+                            // ensure some relationships were selected with a processor as the source
+                            if (nfCanvasUtils.isProcessor(source) || nfCanvasUtils.isProcessor(destination)) {
+                                return getSelectedRelationships().length === 0;
+                            }
+                            return false;
+                        },
+                        handler: {
+                            click: function () {
+                                // see if we're working with a processor as the source
+                                if (nfCanvasUtils.isProcessor(source) || nfCanvasUtils.isProcessor(destination)) {
+                                    // update the selected relationships
+                                    updateConnection(getSelectedRelationships()).done(function () {
+                                        deferred.resolve();
+                                    }).fail(function () {
+                                        deferred.reject();
+                                    });
+                                } else {
+                                    // there are no relationships, but the source wasn't a processor, so update anyway
+                                    updateConnection(undefined).done(function () {
+                                        deferred.resolve();
+                                    }).fail(function () {
+                                        deferred.reject();
+                                    });
+                                }
+
+                                // close the dialog
+                                $('#connection-configuration').modal('hide');
+                            }
+                        }
+                    },
+                        {
+                            buttonText: 'Cancel',
+                            color: {
+                                base: '#E3E8EB',
+                                hover: '#C7D2D7',
+                                text: '#004849'
+                            },
+                            handler: {
+                                click: function () {
+                                    // hide the dialog
+                                    $('#connection-configuration').modal('hide');
+
+                                    // reject the deferred
+                                    deferred.reject();
+                                }
+                            }
+                        }]);
+
+                    // show the details dialog
+                    $('#connection-configuration').modal('setHeaderText', 'Configure Connection').modal('show');
+
+                    // add the ellipsis if necessary
+                    $('#connection-configuration div.relationship-name').ellipsis();
+
+                    // show the border if necessary
+                    var relationshipNames = $('#relationship-names');
+                    if (relationshipNames.is(':visible') && relationshipNames.get(0).scrollHeight > Math.round(relationshipNames.innerHeight())) {
+                        relationshipNames.css('border-width', '1px');
+                    }
+                }).fail(function () {
+                    deferred.reject();
+                });
+            }).promise();
+        }
+    };
+
+    return nfConnectionConfiguration;
+}));
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js
new file mode 100644 (file)
index 0000000..3c595ca
--- /dev/null
@@ -0,0 +1,1990 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* 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.Storage',
+                'nf.Common',
+                'nf.Client',
+                'nf.CanvasUtils',
+                'nf.ProcessGroup',
+                'nf.ProcessGroupConfiguration',
+                'nf.Graph',
+                'nf.Birdseye'],
+            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfStorage, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfProcessGroupConfiguration, nfGraph, nfBirdseye) {
+                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfStorage, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfProcessGroupConfiguration, nfGraph, nfBirdseye));
+            });
+    } 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.Storage'),
+                require('nf.Common'),
+                require('nf.Client'),
+                require('nf.CanvasUtils'),
+                require('nf.ProcessGroup'),
+                require('nf.ProcessGroupConfiguration'),
+                require('nf.Graph'),
+                require('nf.Birdseye')));
+    } else {
+        nf.FlowVersion = factory(root.$,
+            root.nf.ng.Bridge,
+            root.nf.ErrorHandler,
+            root.nf.Dialog,
+            root.nf.Storage,
+            root.nf.Common,
+            root.nf.Client,
+            root.nf.CanvasUtils,
+            root.nf.ProcessGroup,
+            root.nf.ProcessGroupConfiguration,
+            root.nf.Graph,
+            root.nf.Birdseye);
+    }
+}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfStorage, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfProcessGroupConfiguration, 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-label').text('');
+
+        $('#save-flow-version-registry').text('').hide();
+        $('#save-flow-version-bucket').text('').hide();
+
+        $('#save-flow-version-name').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();
+        $('#save-flow-version-change-comments').val('');
+
+        $('#save-flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+    };
+
+    /**
+     * 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([]);
+            localChangesData.setFilterArgs({
+                searchString: ''
+            });
+        }
+
+        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 () {
+        $('#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();
+
+        $('#import-flow-version-registry').text('').hide();
+        $('#import-flow-version-bucket').text('').hide();
+        $('#import-flow-version-name').text('').hide();
+
+        clearFlowVersionsGrid();
+
+        $('#import-flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+
+        $('#import-flow-version-container').hide();
+        $('#import-flow-version-label').text('');
+    };
+
+    /**
+     * Loads the registries into the specified registry combo.
+     *
+     * @param dialog
+     * @param registryCombo
+     * @param bucketCombo
+     * @param flowCombo
+     * @param selectBucket
+     * @param bucketCheck
+     * @returns {deferred}
+     */
+    var loadRegistries = function (dialog, registryCombo, bucketCombo, flowCombo, selectBucket, bucketCheck) {
+        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.registry.name > b.registry.name;
+                });
+
+                $.each(registriesResponse.registries, function (_, registryEntity) {
+                    var registry = registryEntity.registry;
+                    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, flowCombo, selectBucket, bucketCheck)
+                }
+            });
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Loads the buckets for the specified registryIdentifier for the current user.
+     *
+     * @param registryIdentifier
+     * @param bucketCombo
+     * @param flowCombo
+     * @param selectBucket
+     * @param bucketCheck
+     * @returns {*}
+     */
+    var loadBuckets = function (registryIdentifier, bucketCombo, flowCombo, selectBucket, bucketCheck) {
+        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) {
+                    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) {
+                    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)
+                            });
+                        }
+                    }
+                });
+            }
+
+            if (buckets.length === 0) {
+                buckets.push({
+                    text: 'No available buckets',
+                    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
+            bucketCombo.combo('destroy').combo({
+                options: buckets,
+                select: selectBucket
+            });
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Select handler for the registries combo.
+     *
+     * @param dialog
+     * @param selectedOption
+     * @param bucketCombo
+     * @param flowCombo
+     * @param selectBucket
+     * @param bucketCheck
+     */
+    var selectRegistry = function (dialog, selectedOption, bucketCombo, flowCombo, selectBucket, bucketCheck) {
+        var showNoBucketsAvailable = function () {
+            bucketCombo.combo('destroy').combo({
+                options: [{
+                    text: 'No available buckets',
+                    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');
+        };
+
+        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, flowCombo, selectBucket, bucketCheck).fail(function () {
+                showNoBucketsAvailable();
+            });
+        }
+    };
+
+    /**
+     * Select handler for the buckets combo.
+     *
+     * @param selectedOption
+     */
+    var selectBucketSaveFlowVersion = function (selectedOption) {
+        $('#save-flow-version-dialog').modal('refreshButtons');
+    };
+
+    /**
+     * Saves a flow version.
+     * @author: Renu
+     * @desc for lines 390-396: when a dflow is committed, then environment dropdown selection is enabled.
+     * If Env is pre-selected and enabled =>submit button is enabled
+     * @returns {*}
+     */
+    var saveFlowVersion = function () {
+        var processGroupId = $('#save-flow-version-process-group-id').text();
+        var processGroupRevision = $('#save-flow-version-process-group-id').data('revision');
+
+        $('#environmentType').prop('disabled', false);
+        console.log("test submit btn..... ");
+
+        if($('#environmentType').val() && !$('#environmentType').prop("disabled")){
+            $('#operate-submit-btn').prop('disabled', false);
+            console.log("button is enabled bcz env is already selected and not disabled");
+        }
+
+        var saveFlowVersionRequest = {
+            processGroupRevision: nfClient.getRevision({
+                revision: {
+                    version: processGroupRevision.version
+                }
+            }),
+            'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged()
+        };
+
+        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,
+                comments: $('#save-flow-version-change-comments').val()
+            }
+        } else {
+            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: $('#save-flow-version-name-field').val(),
+                description: $('#save-flow-version-description-field').val(),
+                comments: $('#save-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)
+
+
+    };
+
+    /**
+     * 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 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
+        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 flow versions
+        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) {
+            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
+        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);
+    };
+
+    /**
+     * 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 matchesDifferenceType = item['differenceType'].search(filterExp) >= 0;
+            var matchesDifference = item['difference'].search(filterExp) >= 0;
+
+            // 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
+        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"></div>';
+            }
+
+            return markup;
+        };
+
+        // define the column model for local changes
+        var localChangesColumns = [
+            {
+                id: 'componentName',
+                name: 'Component Name',
+                field: 'componentName',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'differenceType',
+                name: 'Change Type',
+                field: 'differenceType',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'difference',
+                name: 'Difference',
+                field: 'difference',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                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: getFilterText()
+        });
+        localChangesData.setFilter(filter);
+
+        // initialize the sort
+        sort({
+            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('componentName', true);
+        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);
+
+                            localChangesTable.closest('.large-dialog').modal('hide');
+                        });
+                    } else {
+                        nfCanvasUtils.showComponent(componentDifference.processGroupId, componentDifference.componentId).done(function () {
+                            localChangesTable.closest('.large-dialog').modal('hide');
+                        });
+                    }
+                }
+            }
+        });
+
+        // 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 () {
+        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').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();
+        var flowCombo = $('#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, flowCombo, selectBucketImportVersion, function (bucketEntity) {
+            return true;
+        }).done(function () {
+            // 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().always(function () {
+                            $('#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) {
+        // clear the flow versions grid
+        clearFlowVersionsGrid();
+
+        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);
+        } else {
+            // mark no flows available
+            $('#import-flow-version-name-combo').combo('destroy').combo({
+                options: [{
+                    text: 'No available flows',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
+        }
+    };
+
+    /**
+     * Imports the selected flow version.
+     */
+    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');
+
+        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
+                }
+            }),
+            'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+            'component': {
+                '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();
+
+            // 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;
+        }
+    };
+
+    /**
+     * Changes the flow version for the currently selected Process Group.
+     *
+     * @returns {deferred}
+     */
+    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');
+        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]);
+
+        // 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 = {
+                'processGroupRevision': nfClient.getRevision({
+                    'revision': {
+                        'version': processGroupRevision.version
+                    }
+                }),
+                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+                '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 () {
+                // initialize the progress bar value
+                updateProgress(0);
+
+                // show the progress dialog
+                $('#change-version-status-dialog').modal('show');
+            }).fail(nfErrorHandler.handleAjaxError);
+        };
+
+        var pollChangeRequest = function () {
+            getChangeRequest().done(processChangeResponse);
+        };
+
+        var getChangeRequest = function () {
+            return $.ajax({
+                type: 'GET',
+                url: changeRequest.uri,
+                dataType: 'json'
+            }).fail(completeChangeRequest).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.');
+
+                // 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(changeRequest)) {
+                $.ajax({
+                    type: 'DELETE',
+                    url: changeRequest.uri + '?' + $.param({
+                        'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged()
+                    }),
+                    dataType: 'json'
+                }).done(function (response) {
+                    changeRequest = response.request;
+
+                    // update the component that was changing
+                    updateProcessGroup(processGroupId);
+
+                    if (nfCommon.isDefinedAndNotNull(changeRequest.failureReason)) {
+                        // hide the progress dialog
+                        $('#change-version-status-dialog').modal('hide');
+
+                        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');
+                                }
+                            }
+                        }]);
+                    }
+                });
+            }
+        };
+
+        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();
+                }, 2000);
+            }
+        };
+
+        submitChangeRequest().done(processChangeResponse);
+    };
+
+    /**
+     * 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/changing current PG... reload/refresh this group/canvas
+
+            $.ajax({
+                type: 'GET',
+                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();
+
+                // 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
+            nfProcessGroup.reload(processGroupId);
+        }
+    };
+
+    /**
+     * 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);
+    };
+
+    /**
+     * Shows local changes for the specified process group.
+     *
+     * @param processGroupId
+     * @param localChangesMessage
+     * @param localChangesTable
+     * @param totalLabel
+     */
+    var loadLocalChanges = function (processGroupId, localChangesMessage, 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([]);
+
+        // 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'
+        }).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);
+
+        return $.when(loadMessage, loadChanges);
+    };
+
+    /**
+     * 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
+                            }
+                        }),
+                        'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+                        '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 + '?' + $.param({
+                                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged()
+                            }),
+                            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(revertRequest.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;
+
+            // 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 ($('#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;
+                            } else {
+                                return true;
+                            }
+                        } else {
+                            return false;
+                        }
+                    },
+                    handler: {
+                        click: function () {
+                            var processGroupId = $('#save-flow-version-process-group-id').text();
+                            saveFlowVersion().done(function (response) {
+                                updateVersionControlInformation(processGroupId, response.versionControlInformation);
+                            });
+
+                            $(this).modal('hide');
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    color: {
+                        base: '#E3E8EB',
+                        hover: '#C7D2D7',
+                        text: '#004849'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+                handler: {
+                    close: function () {
+                        resetSaveFlowVersionDialog();
+                    }
+                }
+            });
+
+            // initialize the import flow version dialog
+            $('#import-flow-version-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                handler: {
+                    close: function () {
+                        resetImportFlowVersionDialog();
+                    }
+                }
+            });
+
+            // 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', []);
+                    }
+                }
+            });
+
+            // 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: 'Show 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();
+            });
+
+            // 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'));
+        },
+
+        /**
+         * Shows the flow version dialog.
+         *
+         * @param processGroupId
+         */
+        showFlowVersionDialog: function (processGroupId) {
+            var focusName = true;
+
+            return $.Deferred(function (deferred) {
+                getVersionControlInformation(processGroupId).done(function (groupVersionControlInformation) {
+                    if (nfCommon.isDefinedAndNotNull(groupVersionControlInformation.versionControlInformation)) {
+                        var versionControlInformation = groupVersionControlInformation.versionControlInformation;
+
+                        // update the registry and bucket visibility
+                        $('#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();
+                        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);
+
+                        // reposition the version label
+                        $('#save-flow-version-label').css('margin-top', '-15px');
+
+                        focusName = false;
+                        deferred.resolve();
+                    } else {
+                        // update the registry and bucket visibility
+                        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();
+
+                        // reposition the version label
+                        $('#save-flow-version-label').css('margin-top', '0');
+
+                        loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, null, selectBucketSaveFlowVersion, function (bucketEntity) {
+                            return bucketEntity.permissions.canWrite === true;
+                        }).done(function () {
+                            deferred.resolve();
+                        }).fail(function () {
+                            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');
+
+                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 local changes for the specified Process Group.
+         *
+         * @param processGroupId
+         */
+        revertLocalChanges: function (processGroupId) {
+            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');
+            });
+        },
+
+        /**
+         * Shows local changes for the specified process group.
+         *
+         * @param processGroupId
+         */
+        showLocalChanges: function (processGroupId) {
+            loadLocalChanges(processGroupId, $('#show-local-changes-message'), $('#show-local-changes-table'), $('#total-show-local-changes-entries')).done(function () {
+                $('#show-local-changes-dialog').modal('show');
+            });
+        },
+
+        /**
+         * Shows the change flow version dialog.
+         *
+         * @param 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.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);
+
+                        // 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 () {
+                // 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();
+                        }
+                    }
+                }, {
+                    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: 'Stop Version Control',
+                dialogContent: 'Are you sure you want to stop version control?',
+                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($.extend({
+                                    'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged()
+                                }, revision)),
+                                dataType: 'json',
+                                contentType: 'application/json'
+                            }).done(function (response) {
+                                updateVersionControlInformation(processGroupId, undefined);
+
+                                nfDialog.showOkDialog({
+                                    headerText: 'Disconnect',
+                                    dialogContent: 'This Process Group is no longer under version control.'
+                                });
+                            }).fail(nfErrorHandler.handleAjaxError);
+                        } else {
+                            nfDialog.showOkDialog({
+                                headerText: 'Disconnect',
+                                dialogContent: 'This Process Group is not currently under version control.'
+                            })
+                        }
+                    }).fail(nfErrorHandler.handleAjaxError);
+                }
+            });
+        }
+    };
+}));
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js
new file mode 100644 (file)
index 0000000..614472c
--- /dev/null
@@ -0,0 +1,1744 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* global d3, define, module, require, exports */
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'd3',
+                'nf.Connection',
+                'nf.Common',
+                'nf.Client',
+                'nf.CanvasUtils',
+                'nf.Dialog'],
+            function ($, d3, nfConnection, nfCommon, nfClient, nfCanvasUtils, nfDialog) {
+                return (nf.ProcessGroup = factory($, d3, nfConnection, nfCommon, nfClient, nfCanvasUtils, nfDialog));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.ProcessGroup =
+            factory(require('jquery'),
+                require('d3'),
+                require('nf.Connection'),
+                require('nf.Common'),
+                require('nf.Client'),
+                require('nf.CanvasUtils'),
+                require('nf.Dialog')));
+    } else {
+        nf.ProcessGroup = factory(root.$,
+            root.d3,
+            root.nf.Connection,
+            root.nf.Common,
+            root.nf.Client,
+            root.nf.CanvasUtils,
+            root.nf.Dialog);
+    }
+}
+(this, function ($, d3, nfConnection, nfCommon, nfClient, nfCanvasUtils, nfDialog) {
+    'use strict';
+
+    var nfConnectable;
+    var nfDraggable;
+    var nfSelectable;
+    var nfContextMenu;
+
+    var PREVIEW_NAME_LENGTH = 30;
+
+    var dimensions = {
+        width: 380,
+        height: 172
+    };
+
+    // ----------------------------
+    // process groups currently on the graph
+    // ----------------------------
+
+    var processGroupMap;
+
+    // -----------------------------------------------------------
+    // cache for components that are added/removed from the canvas
+    // -----------------------------------------------------------
+
+    var removedCache;
+    var addedCache;
+
+    // --------------------
+    // component containers
+    // --------------------
+
+    var processGroupContainer;
+
+    // --------------------------
+    // privately scoped functions
+    // --------------------------
+
+    /**
+     * Determines whether the specified process group is under version control.
+     *
+     * @param d
+     */
+    var isUnderVersionControl = function (d) {
+        return nfCommon.isDefinedAndNotNull(d.versionedFlowState);
+    };
+
+    /**
+     * Selects the process group elements against the current process group map.
+     */
+    var select = function () {
+        return processGroupContainer.selectAll('g.process-group').data(processGroupMap.values(), function (d) {
+            return d.id;
+        });
+    };
+
+
+    /**
+     * Renders the process groups in the specified selection.
+     *
+     * @param {selection} entered           The selection of process groups to be rendered
+     * @param {boolean} selected            Whether the process group should be selected
+     * @return the entered selection
+     */
+    var renderProcessGroups = function (entered, selected) {
+        if (entered.empty()) {
+            return entered;
+        }
+
+        var processGroup = entered.append('g')
+            .attrs({
+                'id': function (d) {
+                    return 'id-' + d.id;
+                },
+                'class': 'process-group component'
+            })
+            .classed('selected', selected)
+            .call(nfCanvasUtils.position);
+
+        // ----
+        // body
+        // ----
+
+        // process group border
+        processGroup.append('rect')
+            .attrs({
+                'class': 'border',
+                'width': function (d) {
+                    return d.dimensions.width;
+                },
+                'height': function (d) {
+                    return d.dimensions.height;
+                },
+                'fill': 'transparent',
+                'stroke': 'transparent'
+            });
+
+        // process group body
+        processGroup.append('rect')
+            .attrs({
+                'class': 'body',
+                'width': function (d) {
+                    return d.dimensions.width;
+                },
+                'height': function (d) {
+                    return d.dimensions.height;
+                },
+                'filter': 'url(#component-drop-shadow)',
+                'stroke-width': 0
+            });
+
+        // process group name background
+        processGroup.append('rect')
+            .attrs({
+                'width': function (d) {
+                    return d.dimensions.width;
+                },
+                'height': 32,
+                'fill': '#b8c6cd'
+            });
+
+        // process group name
+        processGroup.append('text')
+            .attrs({
+                'x': 10,
+                'y': 20,
+                'width': 300,
+                'height': 16,
+                'class': 'process-group-name'
+            });
+
+        // process group name
+        processGroup.append('text')
+            .attrs({
+                'x': 10,
+                'y': 21,
+                'class': 'version-control'
+                   });
+
+            console.log(processGroup);
+
+
+        // always support selecting and navigation
+        processGroup.on('dblclick', function (d) {
+            // enter this group on double click
+            nfProcessGroup.enterGroup(d.id);
+        })
+            .call(nfSelectable.activate).call(nfContextMenu.activate);
+
+        // only support dragging, connection, and drag and drop if appropriate
+        processGroup.filter(function (d) {
+            return d.permissions.canWrite && d.permissions.canRead;
+        })
+            .on('mouseover.drop', function (d) {
+                // Using mouseover/out to workaround chrome issue #122746
+
+                // get the target and ensure its not already been marked for drop
+                var target = d3.select(this);
+                if (!target.classed('drop')) {
+                    var targetData = target.datum();
+
+                    // see if there is a selection being dragged
+                    var drag = d3.select('rect.drag-selection');
+                    if (!drag.empty()) {
+                        // filter the current selection by this group
+                        var selection = nfCanvasUtils.getSelection().filter(function (d) {
+                            return targetData.id === d.id;
+                        });
+
+                        // ensure this group isn't in the selection
+                        if (selection.empty()) {
+                            // mark that we are hovering over a drop area if appropriate
+                            target.classed('drop', function () {
+                                // get the current selection and ensure its disconnected
+                                return nfConnection.isDisconnected(nfCanvasUtils.getSelection());
+                            });
+                        }
+                    }
+                }
+            })
+            .on('mouseout.drop', function (d) {
+                // mark that we are no longer hovering over a drop area unconditionally
+                d3.select(this).classed('drop', false);
+            })
+            .call(nfDraggable.activate)
+            .call(nfConnectable.activate);
+
+        return processGroup;
+    };
+
+    // attempt of space between component count and icon for process group contents
+    var CONTENTS_SPACER = 10;
+    var CONTENTS_VALUE_SPACER = 5;
+
+    /**
+     * Updates the process groups in the specified selection.
+     *
+     * @param {selection} updated               The process groups to be updated
+     */
+    var updateProcessGroups = function (updated) {
+        if (updated.empty()) {
+            return;
+        }
+
+        // process group border authorization
+        updated.select('rect.border')
+            .classed('unauthorized', function (d) {
+                return d.permissions.canRead === false;
+            });
+
+        // process group body authorization
+        updated.select('rect.body')
+            .classed('unauthorized', function (d) {
+                return d.permissions.canRead === false;
+            });
+
+        updated.each(function (processGroupData) {
+            var processGroup = d3.select(this);
+            var details = processGroup.select('g.process-group-details');
+
+            // update the component behavior as appropriate
+            nfCanvasUtils.editable(processGroup, nfConnectable, nfDraggable);
+
+            // if this processor is visible, render everything
+            if (processGroup.classed('visible')) {
+                if (details.empty()) {
+                    details = processGroup.append('g').attr('class', 'process-group-details');
+
+                    // -------------------
+                    // contents background
+                    // -------------------
+
+                    details.append('rect')
+                        .attrs({
+                            'x': 0,
+                            'y': 32,
+                            'width': function () {
+                                return processGroupData.dimensions.width
+                            },
+                            'height': 24,
+                            'fill': '#e3e8eb'
+                        });
+
+                    details.append('rect')
+                        .attrs({
+                            'x': 0,
+                            'y': function () {
+                                return processGroupData.dimensions.height - 24;
+                            },
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 24,
+                            'fill': '#e3e8eb'
+                        });
+
+                    // --------
+                    // contents
+                    // --------
+
+                    // transmitting icon
+//                    details.append('text')
+//                        .attrs({
+//                            'x': 10,
+//                            'y': 49,
+//                            'class': 'process-group-transmitting process-group-contents-icon',
+//                            'font-family': 'FontAwesome'
+//                        })
+//                        .text('\uf140')
+//                        .append("title")
+//                        .text("Transmitting Remote Process Groups");
+
+
+                    // transmitting count
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-transmitting-count process-group-contents-count'
+//                        });
+
+                    // not transmitting icon
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-not-transmitting process-group-contents-icon',
+//                            'font-family': 'flowfont'
+//                        })
+//                        .text('\ue80a')
+//                        .append("title")
+//                        .text("Not Transmitting Remote Process Groups");
+
+                    // not transmitting count
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-not-transmitting-count process-group-contents-count'
+//                        });
+
+                    // running icon
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-running process-group-contents-icon',
+//                            'font-family': 'FontAwesome'
+//                        })
+//                        .text('\uf04b')
+//                        .append("title")
+//                        .text("Running Components");
+
+                    // running count
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-running-count process-group-contents-count'
+//                        });
+
+                    // stopped icon
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-stopped process-group-contents-icon',
+//                            'font-family': 'FontAwesome'
+//                        })
+//                        .text('\uf04d')
+//                        .append("title")
+//                        .text("Stopped Components");
+
+                    // stopped count
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-stopped-count process-group-contents-count'
+//                        });
+
+                    // invalid icon
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-invalid process-group-contents-icon',
+//                            'font-family': 'FontAwesome'
+//                        })
+//                        .text('\uf071')
+//                        .append("title")
+//                        .text("Invalid Components");
+
+                    // invalid count
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-invalid-count process-group-contents-count'
+//                        });
+
+                    // disabled icon
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-disabled process-group-contents-icon',
+//                            'font-family': 'flowfont'
+//                        })
+//                        .text('\ue802')
+//                        .append("title")
+//                        .text("Disabled Components");
+
+                    // disabled count
+//                    details.append('text')
+//                        .attrs({
+//                            'y': 49,
+//                            'class': 'process-group-disabled-count process-group-contents-count'
+//                        });
+
+                    // up to date icon
+                    details.append('text')
+                        .attrs({
+                            'x': 10,
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-up-to-date process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf00c')
+                        .append("title")
+                        .text("Up to date Versioned Process Groups");
+
+                    // up to date count
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-up-to-date-count process-group-contents-count'
+                        });
+
+                    // locally modified icon
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf069')
+                        .append("title")
+                        .text("Locally modified Versioned Process Groups");
+
+                    // locally modified count
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified-count process-group-contents-count'
+                        });
+
+                    // stale icon
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-stale process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf0aa')
+                        .append("title")
+                        .text("Stale Versioned Process Groups");
+
+                    // stale count
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-stale-count process-group-contents-count'
+                        });
+
+                    // locally modified and stale icon
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified-and-stale process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf06a')
+                        .append("title")
+                        .text("Locally modified and stale Versioned Process Groups");
+
+                    // locally modified and stale count
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-locally-modified-and-stale-count process-group-contents-count'
+                        });
+
+                    // sync failure icon
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-sync-failure process-group-contents-icon',
+                            'font-family': 'FontAwesome'
+                        })
+                        .text('\uf128')
+                        .append("title")
+                        .text("Sync failure Versioned Process Groups");
+
+                    // sync failure count
+                    details.append('text')
+                        .attrs({
+                            'y': function () {
+                                return processGroupData.dimensions.height - 7;
+                            },
+                            'class': 'process-group-sync-failure-count process-group-contents-count'
+                        });
+
+                    // ----------------
+                    // stats background
+                    // ----------------
+
+                    // queued
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 19,
+                            'x': 0,
+                            'y': 66,
+                            'fill': '#f4f6f7'
+                        });
+
+                    // border
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 1,
+                            'x': 0,
+                            'y': 84,
+                            'fill': '#c7d2d7'
+                        });
+
+                    // in
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 19,
+                            'x': 0,
+                            'y': 85,
+                            'fill': '#ffffff'
+                        });
+
+                    // border
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 1,
+                            'x': 0,
+                            'y': 103,
+                            'fill': '#c7d2d7'
+                        });
+
+                    // read/write
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 19,
+                            'x': 0,
+                            'y': 104,
+                            'fill': '#f4f6f7'
+                        });
+
+                    // border
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 1,
+                            'x': 0,
+                            'y': 122,
+                            'fill': '#c7d2d7'
+                        });
+
+                    // out
+                    details.append('rect')
+                        .attrs({
+                            'width': function () {
+                                return processGroupData.dimensions.width;
+                            },
+                            'height': 19,
+                            'x': 0,
+                            'y': 123,
+                            'fill': '#ffffff'
+                        });
+
+                    // -----
+                    // stats
+                    // -----
+
+                    // stats label container
+                    var processGroupStatsLabel = details.append('g')
+                        .attrs({
+                            'transform': 'translate(6, 75)'
+                        });
+
+                    // queued label
+                    processGroupStatsLabel.append('text')
+                        .attrs({
+                            'width': 73,
+                            'height': 10,
+                            'x': 4,
+                            'y': 5,
+                            'class': 'stats-label'
+                        })
+                        .text('Queued');
+
+                    // in label
+                    processGroupStatsLabel.append('text')
+                        .attrs({
+                            'width': 73,
+                            'height': 10,
+                            'x': 4,
+                            'y': 24,
+                            'class': 'stats-label'
+                        })
+                        .text('In');
+
+                    // read/write label
+                    processGroupStatsLabel.append('text')
+                        .attrs({
+                            'width': 73,
+                            'height': 10,
+                            'x': 4,
+                            'y': 42,
+                            'class': 'stats-label'
+                        })
+                        .text('Read/Write');
+
+                    // out label
+                    processGroupStatsLabel.append('text')
+                        .attrs({
+                            'width': 73,
+                            'height': 10,
+                            'x': 4,
+                            'y': 60,
+                            'class': 'stats-label'
+                        })
+                        .text('Out');
+
+                    // stats value container
+                    var processGroupStatsValue = details.append('g')
+                        .attrs({
+                            'transform': 'translate(95, 75)'
+                        });
+
+                    // queued value
+                    var queuedText = processGroupStatsValue.append('text')
+                        .attrs({
+                            'width': 180,
+                            'height': 10,
+                            'x': 4,
+                            'y': 5,
+                            'class': 'process-group-queued stats-value'
+                        });
+
+                    // queued count
+                    queuedText.append('tspan')
+                        .attrs({
+                            'class': 'count'
+                        });
+
+                    // queued size
+                    queuedText.append('tspan')
+                        .attrs({
+                            'class': 'size'
+                        });
+
+                    // in value
+                    var inText = processGroupStatsValue.append('text')
+                        .attrs({
+                            'width': 180,
+                            'height': 10,
+                            'x': 4,
+                            'y': 24,
+                            'class': 'process-group-in stats-value'
+                        });
+
+                    // in count
+                    inText.append('tspan')
+                        .attrs({
+                            'class': 'count'
+                        });
+
+                    // in size
+                    inText.append('tspan')
+                        .attrs({
+                            'class': 'size'
+                        });
+
+                    // in
+                    inText.append('tspan')
+                        .attrs({
+                            'class': 'ports'
+                        });
+
+                    // read/write value
+                    processGroupStatsValue.append('text')
+                        .attrs({
+                            'width': 180,
+                            'height': 10,
+                            'x': 4,
+                            'y': 42,
+                            'class': 'process-group-read-write stats-value'
+                        });
+
+                    // out value
+                    var outText = processGroupStatsValue.append('text')
+                        .attrs({
+                            'width': 180,
+                            'height': 10,
+                            'x': 4,
+                            'y': 60,
+                            'class': 'process-group-out stats-value'
+                        });
+
+                    // out ports
+                    outText.append('tspan')
+                        .attrs({
+                            'class': 'ports'
+                        });
+
+                    // out count
+                    outText.append('tspan')
+                        .attrs({
+                            'class': 'count'
+                        });
+
+                    // out size
+                    outText.append('tspan')
+                        .attrs({
+                            'class': 'size'
+                        });
+
+                    // stats value container
+                    var processGroupStatsInfo = details.append('g')
+                        .attrs({
+                            'transform': 'translate(335, 75)'
+                        });
+
+                    // in info
+                    processGroupStatsInfo.append('text')
+                        .attrs({
+                            'width': 25,
+                            'height': 10,
+                            'x': 4,
+                            'y': 24,
+                            'class': 'stats-info'
+                        })
+                        .text('5 min');
+
+                    // read/write info
+                    processGroupStatsInfo.append('text')
+                        .attrs({
+                            'width': 25,
+                            'height': 10,
+                            'x': 4,
+                            'y': 42,
+                            'class': 'stats-info'
+                        })
+                        .text('5 min');
+
+                    // out info
+                    processGroupStatsInfo.append('text')
+                        .attrs({
+                            'width': 25,
+                            'height': 10,
+                            'x': 4,
+                            'y': 60,
+                            'class': 'stats-info'
+                        })
+                        .text('5 min');
+
+                    // --------
+                    // comments
+                    // --------
+
+                    details.append('path')
+                        .attrs({
+                            'class': 'component-comments',
+                            'transform': 'translate(' + (processGroupData.dimensions.width - 2) + ', ' + (processGroupData.dimensions.height - 10) + ')',
+                            'd': 'm0,0 l0,8 l-8,0 z'
+                        });
+
+                    // -------------------
+                    // active thread count
+                    // -------------------
+
+                    // active thread count
+                    details.append('text')
+                        .attrs({
+                            'class': 'active-thread-count-icon',
+                            'y': 20
+                        })
+                        .text('\ue83f');
+
+                    // active thread icon
+                    details.append('text')
+                        .attrs({
+                            'class': 'active-thread-count',
+                            'y': 20
+                        });
+
+                    // ---------
+                    // bulletins
+                    // ---------
+
+                    // bulletin background
+                    details.append('rect')
+                        .attrs({
+                            'class': 'bulletin-background',
+                            'x': function () {
+                                return processGroupData.dimensions.width - 24;
+                            },
+                            'y': 32,
+                            'width': 24,
+                            'height': 24
+                        });
+
+                    // bulletin icon
+                    details.append('text')
+                        .attrs({
+                            'class': 'bulletin-icon',
+                            'x': function () {
+                                return processGroupData.dimensions.width - 17;
+                            },
+                            'y': 49
+                        })
+                        .text('\uf24a');
+                }
+
+                // update transmitting
+                var transmitting = details.select('text.process-group-transmitting')
+                    .classed('transmitting', function (d) {
+                        return d.permissions.canRead && d.activeRemotePortCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.activeRemotePortCount === 0;
+                    });
+                var transmittingCount = details.select('text.process-group-transmitting-count')
+                    .attr('x', function () {
+                        var transmittingCountX = parseInt(transmitting.attr('x'), 10);
+                        return transmittingCountX + Math.round(transmitting.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.activeRemotePortCount;
+                    });
+                transmittingCount.append("title").text("Transmitting Remote Process Groups");
+
+                // update not transmitting
+                var notTransmitting = details.select('text.process-group-not-transmitting')
+                    .classed('not-transmitting', function (d) {
+                        return d.permissions.canRead && d.inactiveRemotePortCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.inactiveRemotePortCount === 0;
+                    })
+                    .attr('x', function () {
+                        var transmittingX = parseInt(transmittingCount.attr('x'), 10);
+                        return transmittingX + Math.round(transmittingCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var notTransmittingCount = details.select('text.process-group-not-transmitting-count')
+                    .attr('x', function () {
+                        var notTransmittingCountX = parseInt(notTransmitting.attr('x'), 10);
+                        return notTransmittingCountX + Math.round(notTransmitting.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.inactiveRemotePortCount;
+                    });
+                notTransmittingCount.append("title").text("Not transmitting Remote Process Groups")
+
+                // update running
+                var running = details.select('text.process-group-running')
+                    .classed('running', function (d) {
+                        return d.permissions.canRead && d.component.runningCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.runningCount === 0;
+                    })
+                    .attr('x', function () {
+                        var notTransmittingX = parseInt(notTransmittingCount.attr('x'), 10);
+                        return notTransmittingX + Math.round(notTransmittingCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var runningCount = details.select('text.process-group-running-count')
+                    .attr('x', function () {
+                        var runningCountX = parseInt(running.attr('x'), 10);
+                        return runningCountX + Math.round(running.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.runningCount;
+                    });
+                runningCount.append("title").text("Running Components");
+
+                // update stopped
+                var stopped = details.select('text.process-group-stopped')
+                    .classed('stopped', function (d) {
+                        return d.permissions.canRead && d.component.stoppedCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.stoppedCount === 0;
+                    })
+                    .attr('x', function () {
+                        var runningX = parseInt(runningCount.attr('x'), 10);
+                        return runningX + Math.round(runningCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var stoppedCount = details.select('text.process-group-stopped-count')
+                    .attr('x', function () {
+                        var stoppedCountX = parseInt(stopped.attr('x'), 10);
+                        return stoppedCountX + Math.round(stopped.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.stoppedCount;
+                    });
+                stoppedCount.append("title").text("Stopped Components");
+
+                // update invalid
+                var invalid = details.select('text.process-group-invalid')
+                    .classed('invalid', function (d) {
+                        return d.permissions.canRead && d.component.invalidCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.invalidCount === 0;
+                    })
+                    .attr('x', function () {
+                        var stoppedX = parseInt(stoppedCount.attr('x'), 10);
+                        return stoppedX + Math.round(stoppedCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var invalidCount = details.select('text.process-group-invalid-count')
+                    .attr('x', function () {
+                        var invalidCountX = parseInt(invalid.attr('x'), 10);
+                        return invalidCountX + Math.round(invalid.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.invalidCount;
+                    });
+                invalidCount.append("title").text("Invalid Components");
+
+                // update disabled
+                var disabled = details.select('text.process-group-disabled')
+                    .classed('disabled', function (d) {
+                        return d.permissions.canRead && d.component.disabledCount > 0;
+                    })
+                    .classed('zero', function (d) {
+                        return d.permissions.canRead && d.component.disabledCount === 0;
+                    })
+                    .attr('x', function () {
+                        var invalidX = parseInt(invalidCount.attr('x'), 10);
+                        return invalidX + Math.round(invalidCount.node().getComputedTextLength()) + CONTENTS_SPACER;
+                    });
+                var disabledCount = details.select('text.process-group-disabled-count')
+                    .attr('x', function () {
+                        var disabledCountX = parseInt(disabled.attr('x'), 10);
+                        return disabledCountX + Math.round(disabled.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER;
+                    })
+                    .text(function (d) {
+                        return d.disabledCount;
+                    });
+                disabledCount.append("title").text("Disabled Components");
+
+                // 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;
+                    });
+                upToDateCount.append("title").text("Up to date Versioned Process Groups");
+
+                // 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;
+                    });
+                locallyModifiedCount.append("title").text("Locally modified Versioned Process Groups");
+
+                // 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;
+                    });
+                staleCount.append("title").text("Stale Versioned Process Groups");
+
+                // 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;
+                    });
+                locallyModifiedAndStaleCount.append("title").text("Locally modified and stale Versioned Process Groups");
+
+                // 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;
+                    });
+                var syncFailureCount = 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;
+                    });
+                syncFailureCount.append("title").text("Sync failure Versioned Process Groups");
+
+                /**
+                *  update version control information
+                * @author: Renu
+                * @desc for lines 1110-1201: based on state of the process group, environment selection and submit button enable/disable
+                */
+                var versionControl = processGroup.select('text.version-control')
+                    .styles({
+                        'visibility': isUnderVersionControl(processGroupData) ? 'visible' : 'hidden',
+                        'fill': function () {
+                            if (isUnderVersionControl(processGroupData)) {
+                                var vciState = processGroupData.versionedFlowState;
+                                if (vciState === 'SYNC_FAILURE') {
+                                          $('#environmentType').prop('disabled', true);
+                                            if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                                $('#operate-submit-btn').prop('disabled', false);
+                                           }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                                    return '#666666';
+                                } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
+                                     console.log("locally but stale in style");
+                                    $('#environmentType').prop('disabled', true);
+                                       if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                           $('#operate-submit-btn').prop('disabled', false);
+                                      }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                                    return '#BA554A';
+                                } else if (vciState === 'STALE') {
+                                     console.log("stale in style");
+                                       $('#environmentType').prop('disabled', true);
+                                       if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                           $('#operate-submit-btn').prop('disabled', false);
+                                      }else{$('#operate-submit-btn').prop('disabled', true);}
+                                                                          return '#BA554A';
+                                } else if (vciState === 'LOCALLY_MODIFIED') {
+                                 console.log("locally modified in style");
+                                          $('#environmentType').prop('disabled', true);
+                                          if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                                 $('#operate-submit-btn').prop('disabled', false);
+                                          }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                                    return '#666666';
+                                } else {
+                                    return '#1A9964';
+                                  $('#environmentType').prop('disabled', false);
+                                   if($('#environmentType').val() &&  !$('#environmentType').prop('disabled')){
+                                        $('#operate-submit-btn').prop('disabled', false);
+                                     }else{$('#operate-submit-btn').prop('disabled', true);}
+                                }
+                            } else {
+                                $('#environmentType').prop('disabled', true);
+                                return '#000';
+                            }
+                        }
+                    })
+                    .text(function () {
+                        if (isUnderVersionControl(processGroupData)) {
+                            var vciState = processGroupData.versionedFlowState;
+                            if (vciState === 'SYNC_FAILURE') {
+                                    $('#environmentType').prop('disabled', true);
+                                             if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                                 $('#operate-submit-btn').prop('disabled', false);
+                                            }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                                return '\uf128'
+                            } else if (vciState === 'LOCALLY_MODIFIED_AND_STALE') {
+                              console.log("locally but stale in text");
+                                     $('#environmentType').prop('disabled', true);
+                                       if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                           $('#operate-submit-btn').prop('disabled', false);
+                                      }else{$('#operate-submit-btn').prop('disabled', true);}
+
+                                return '\uf06a';
+                            } else if (vciState === 'STALE') {
+                               console.log("stale in text");
+                                   $('#environmentType').prop('disabled', true);
+                                       if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                           $('#operate-submit-btn').prop('disabled', false);
+                                      }else{$('#operate-submit-btn').prop('disabled', true);}
+                               return '\uf0aa';
+                            } else if (vciState === 'LOCALLY_MODIFIED') {
+                                console.log("locally modified in text");
+                                        $('#environmentType').prop('disabled', true);
+                                        if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                            $('#operate-submit-btn').prop('disabled', false);
+                                       }else{$('#operate-submit-btn').prop('disabled', true);}
+                               return '\uf069';
+                            } else {
+                                return '\uf00c';
+                                $('#environmentType').prop('disabled', false);
+                                if($('#environmentType').val() && !$('#environmentType').prop('disabled')){
+                                   $('#operate-submit-btn').prop('disabled', false);
+                                }else{$('#operate-submit-btn').prop('disabled', true);}
+                            }
+                        } else {
+                            $('#environmentType').prop('disabled', true);
+                            return '';
+                        }
+                    });
+
+                if (processGroupData.permissions.canRead) {
+                    // version control tooltip
+                    versionControl.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
+                    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);
+
+                            // 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')
+                        .attrs({
+                            '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 300 - (processGroupNameX - versionControlX);
+                                } else {
+                                    return 300;
+                                }
+                            }
+                        })
+                        .each(function (d) {
+                            var processGroupName = d3.select(this);
+
+                            // reset the process group name to handle any previous state
+                            processGroupName.text(null).selectAll('title').remove();
+
+                            // apply ellipsis to the process group name as necessary
+                            nfCanvasUtils.ellipsis(processGroupName, d.component.name);
+                        })
+                        .append('title')
+                        .text(function (d) {
+                            return d.component.name;
+                        });
+                } else {
+                    // clear the process group comments
+                    processGroup.select('path.component-comments').style('visibility', 'hidden');
+
+                    // clear the process group name
+                    processGroup.select('text.process-group-name')
+                        .attrs({
+                            'x': 10,
+                            'width': 316
+                        })
+                        .text(null);
+
+                    // clear tooltips
+                    processGroup.call(removeTooltips);
+                }
+
+                // populate the stats
+                processGroup.call(updateProcessGroupStatus);
+            } else {
+                if (processGroupData.permissions.canRead) {
+                    // update the process group name
+                    processGroup.select('text.process-group-name')
+                        .text(function (d) {
+                            var name = d.component.name;
+                            if (name.length > PREVIEW_NAME_LENGTH) {
+                                return name.substring(0, PREVIEW_NAME_LENGTH) + String.fromCharCode(8230);
+                            } else {
+                                return name;
+                            }
+                        });
+                } else {
+                    // clear the process group name
+                    processGroup.select('text.process-group-name').text(null);
+                }
+
+                // remove the tooltips
+                processGroup.call(removeTooltips);
+
+                // remove the details if necessary
+                if (!details.empty()) {
+                    details.remove();
+                }
+            }
+        });
+    };
+
+    /**
+     * Updates the process group status.
+     *
+     * @param {selection} updated           The process groups to be updated
+     */
+    var updateProcessGroupStatus = function (updated) {
+        if (updated.empty()) {
+            return;
+        }
+
+        // queued count value
+        updated.select('text.process-group-queued tspan.count')
+            .text(function (d) {
+                return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.queued, ' ');
+            });
+
+        // queued size value
+        updated.select('text.process-group-queued tspan.size')
+            .text(function (d) {
+                return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.queued, ' ');
+            });
+
+        // in count value
+        updated.select('text.process-group-in tspan.count')
+            .text(function (d) {
+                return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.input, ' ');
+            });
+
+        // in size value
+        updated.select('text.process-group-in tspan.size')
+            .text(function (d) {
+                return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.input, ' ');
+            });
+
+        // in ports value
+        updated.select('text.process-group-in tspan.ports')
+            .text(function (d) {
+                return ' ' + String.fromCharCode(8594) + ' ' + d.inputPortCount;
+            });
+
+        // read/write value
+        updated.select('text.process-group-read-write')
+            .text(function (d) {
+                return d.status.aggregateSnapshot.read + ' / ' + d.status.aggregateSnapshot.written;
+            });
+
+        // out ports value
+        updated.select('text.process-group-out tspan.ports')
+            .text(function (d) {
+                return d.outputPortCount + ' ' + String.fromCharCode(8594) + ' ';
+            });
+
+        // out count value
+        updated.select('text.process-group-out tspan.count')
+            .text(function (d) {
+                return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.output, ' ');
+            });
+
+        // out size value
+        updated.select('text.process-group-out tspan.size')
+            .text(function (d) {
+                return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.output, ' ');
+            });
+
+        updated.each(function (d) {
+            var processGroup = d3.select(this);
+            var offset = 0;
+
+            // -------------------
+            // active thread count
+            // -------------------
+
+            nfCanvasUtils.activeThreadCount(processGroup, d, function (off) {
+                offset = off;
+            });
+
+            // ---------
+            // bulletins
+            // ---------
+
+            processGroup.select('rect.bulletin-background').classed('has-bulletins', function () {
+                return !nfCommon.isEmpty(d.status.aggregateSnapshot.bulletins);
+            });
+
+            nfCanvasUtils.bulletins(processGroup, d, function () {
+                return d3.select('#process-group-tooltips');
+            }, offset);
+        });
+    };
+
+    /**
+     * Removes the process groups in the specified selection.
+     *
+     * @param {selection} removed               The process groups to be removed
+     */
+    var removeProcessGroups = function (removed) {
+        if (removed.empty()) {
+            return;
+        }
+
+        removed.call(removeTooltips).remove();
+    };
+
+    /**
+     * Removes the tooltips for the process groups in the specified selection.
+     *
+     * @param {selection} removed
+     */
+    var removeTooltips = function (removed) {
+        removed.each(function (d) {
+            // remove any associated tooltips
+            $('#bulletin-tip-' + d.id).remove();
+            $('#version-control-tip-' + d.id).remove();
+            $('#comments-tip-' + d.id).remove();
+        });
+    };
+
+    var nfProcessGroup = {
+        /**
+         * Initializes of the Process Group handler.
+         *
+         * @param nfConnectableRef   The nfConnectable module.
+         * @param nfDraggableRef   The nfDraggable module.
+         * @param nfSelectableRef   The nfSelectable module.
+         * @param nfContextMenuRef   The nfContextMenu module.
+         */
+        init: function (nfConnectableRef, nfDraggableRef, nfSelectableRef, nfContextMenuRef) {
+            nfConnectable = nfConnectableRef;
+            nfDraggable = nfDraggableRef;
+            nfSelectable = nfSelectableRef;
+            nfContextMenu = nfContextMenuRef;
+
+            processGroupMap = d3.map();
+            removedCache = d3.map();
+            addedCache = d3.map();
+
+            // create the process group container
+            processGroupContainer = d3.select('#canvas').append('g')
+                .attrs({
+                    'pointer-events': 'all',
+                    'class': 'process-groups'
+                });
+        },
+
+        /**
+         * Adds the specified process group entity.
+         *
+         * @param processGroupEntities       The process group
+         * @param options           Configuration options
+         */
+        add: function (processGroupEntities, options) {
+            var selectAll = false;
+            if (nfCommon.isDefinedAndNotNull(options)) {
+                selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll;
+            }
+
+            // get the current time
+            var now = new Date().getTime();
+
+            var add = function (processGroupEntity) {
+                addedCache.set(processGroupEntity.id, now);
+
+                // add the process group
+                processGroupMap.set(processGroupEntity.id, $.extend({
+                    type: 'ProcessGroup',
+                    dimensions: dimensions
+                }, processGroupEntity));
+            };
+
+            // determine how to handle the specified process groups
+            if ($.isArray(processGroupEntities)) {
+                $.each(processGroupEntities, function (_, processGroupEntity) {
+                    add(processGroupEntity);
+                });
+            } else if (nfCommon.isDefinedAndNotNull(processGroupEntities)) {
+                add(processGroupEntities);
+            }
+
+            // select
+            var selection = select();
+
+            // enter
+            var entered = renderProcessGroups(selection.enter(), selectAll);
+
+            // update
+            updateProcessGroups(selection.merge(entered));
+        },
+
+        /**
+         * Populates the graph with the specified process groups.
+         *
+         * @argument {object | array} processGroupEntities                    The process groups to add
+         * @argument {object} options                Configuration options
+         */
+        set: function (processGroupEntities, options) {
+            var selectAll = false;
+            var transition = false;
+            var overrideRevisionCheck = false;
+            if (nfCommon.isDefinedAndNotNull(options)) {
+                selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll;
+                transition = nfCommon.isDefinedAndNotNull(options.transition) ? options.transition : transition;
+                overrideRevisionCheck = nfCommon.isDefinedAndNotNull(options.overrideRevisionCheck) ? options.overrideRevisionCheck : overrideRevisionCheck;
+            }
+
+            var set = function (proposedProcessGroupEntity) {
+                var currentProcessGroupEntity = processGroupMap.get(proposedProcessGroupEntity.id);
+
+                // set the process group if appropriate due to revision and wasn't previously removed
+                if ((nfClient.isNewerRevision(currentProcessGroupEntity, proposedProcessGroupEntity) && !removedCache.has(proposedProcessGroupEntity.id)) || overrideRevisionCheck === true) {
+                    processGroupMap.set(proposedProcessGroupEntity.id, $.extend({
+                        type: 'ProcessGroup',
+                        dimensions: dimensions
+                    }, proposedProcessGroupEntity));
+                }
+            };
+
+            // determine how to handle the specified process groups
+            if ($.isArray(processGroupEntities)) {
+                $.each(processGroupMap.keys(), function (_, key) {
+                    var currentProcessGroupEntity = processGroupMap.get(key);
+                    var isPresent = $.grep(processGroupEntities, function (proposedProcessGroupEntity) {
+                        return proposedProcessGroupEntity.id === currentProcessGroupEntity.id;
+                    });
+
+                    // if the current process group is not present and was not recently added, remove it
+                    if (isPresent.length === 0 && !addedCache.has(key)) {
+                        processGroupMap.remove(key);
+                    }
+                });
+                $.each(processGroupEntities, function (_, processGroupEntity) {
+                    set(processGroupEntity);
+                });
+            } else if (nfCommon.isDefinedAndNotNull(processGroupEntities)) {
+                set(processGroupEntities);
+            }
+
+            // select
+            var selection = select();
+
+            // enter
+            var entered = renderProcessGroups(selection.enter(), selectAll);
+
+            // update
+            var updated = selection.merge(entered);
+            updated.call(updateProcessGroups).call(nfCanvasUtils.position, transition);
+
+            // exit
+            selection.exit().call(removeProcessGroups);
+        },
+
+        /**
+         * If the process group id is specified it is returned. If no process group id
+         * specified, all process groups are returned.
+         *
+         * @param {string} id
+         */
+        get: function (id) {
+            if (nfCommon.isUndefined(id)) {
+                return processGroupMap.values();
+            } else {
+                return processGroupMap.get(id);
+            }
+        },
+
+        /**
+         * If the process group id is specified it is refresh according to the current
+         * state. If no process group id is specified, all process groups are refreshed.
+         *
+         * @param {string} id      Optional
+         */
+        refresh: function (id) {
+            if (nfCommon.isDefinedAndNotNull(id)) {
+                d3.select('#id-' + id).call(updateProcessGroups);
+            } else {
+                d3.selectAll('g.process-group').call(updateProcessGroups);
+            }
+        },
+
+        /**
+         * Refreshes the components necessary after a pan event.
+         */
+        pan: function () {
+            d3.selectAll('g.process-group.entering, g.process-group.leaving').call(updateProcessGroups);
+        },
+
+        /**
+         * Reloads the process group state from the server and refreshes the UI.
+         * If the process group is currently unknown, this function reloads the canvas.
+         *
+         * @param {string} id The process group id
+         */
+        reload: function (id) {
+            if (processGroupMap.has(id)) {
+                var processGroupEntity = processGroupMap.get(id);
+                return $.ajax({
+                    type: 'GET',
+                    url: processGroupEntity.uri,
+                    dataType: 'json'
+                }).done(function (response) {
+                    nfProcessGroup.set(response);
+                });
+            }
+        },
+
+        /**
+         * Positions the component.
+         *
+         * @param {string} id   The id
+         */
+        position: function (id) {
+            d3.select('#id-' + id).call(nfCanvasUtils.position);
+        },
+
+        /**
+         * Removes the specified process group.
+         *
+         * @param {string} processGroupIds      The process group id(s)
+         */
+        remove: function (processGroupIds) {
+            var now = new Date().getTime();
+
+            if ($.isArray(processGroupIds)) {
+                $.each(processGroupIds, function (_, processGroupId) {
+                    removedCache.set(processGroupId, now);
+                    processGroupMap.remove(processGroupId);
+                });
+            } else {
+                removedCache.set(processGroupIds, now);
+                processGroupMap.remove(processGroupIds);
+            }
+
+            // apply the selection and handle all removed process groups
+            select().exit().call(removeProcessGroups);
+        },
+
+        /**
+         * Removes all process groups.
+         */
+        removeAll: function () {
+            nfProcessGroup.remove(processGroupMap.keys());
+        },
+
+        /**
+         * Expires the caches up to the specified timestamp.
+         *
+         * @param timestamp
+         */
+        expireCaches: function (timestamp) {
+            var expire = function (cache) {
+                cache.each(function (entryTimestamp, id) {
+                    if (timestamp > entryTimestamp) {
+                        cache.remove(id);
+                    }
+                });
+            };
+
+            expire(addedCache);
+            expire(removedCache);
+        },
+
+        /**
+         * Enters the specified group.
+         *
+         * @param {string} groupId
+         */
+        enterGroup: function (groupId) {
+
+            // hide the context menu
+            nfContextMenu.hide();
+
+            // set the new group id
+            nfCanvasUtils.setGroupId(groupId);
+
+            // reload the graph
+            return nfCanvasUtils.reload().done(function () {
+
+                // attempt to restore the view
+                var viewRestored = nfCanvasUtils.restoreUserView();
+
+                // if the view was not restore attempt to fit
+                if (viewRestored === false) {
+                    nfCanvasUtils.fitCanvas();
+                }
+
+                // update URL deep linking params
+                nfCanvasUtils.setURLParameters(groupId, d3.select());
+
+            }).fail(function () {
+                nfDialog.showOkDialog({
+                    headerText: 'Process Group',
+                    dialogContent: 'Unable to enter the selected group.'
+                });
+            });
+        }
+    };
+
+    return nfProcessGroup;
+}));
diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js
new file mode 100644 (file)
index 0000000..8c61dac
--- /dev/null
@@ -0,0 +1,2373 @@
+/*
+ * 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.
+ *
+ * Modifications to the original nifi code for the ONAP project are made
+ * available under the Apache License, Version 2.0
+ */
+
+/* global define, module, require, exports */
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'Slick',
+                'd3',
+                'nf.Client',
+                'nf.Dialog',
+                'nf.Storage',
+                'nf.Common',
+                'nf.CanvasUtils',
+                'nf.ControllerServices',
+                'nf.ErrorHandler',
+                'nf.FilteredDialogCommon',
+                'nf.ReportingTask',
+                'nf.Shell',
+                'nf.ComponentState',
+                'nf.ComponentVersion',
+                'nf.PolicyManagement'],
+            function ($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfControllerServices, nfErrorHandler, nfFilteredDialogCommon, nfReportingTask, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement) {
+                return (nf.Settings = factory($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfControllerServices, nfErrorHandler, nfFilteredDialogCommon, nfReportingTask, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.Settings =
+            factory(require('jquery'),
+                require('Slick'),
+                require('d3'),
+                require('nf.Client'),
+                require('nf.Dialog'),
+                require('nf.Storage'),
+                require('nf.Common'),
+                require('nf.CanvasUtils'),
+                require('nf.ControllerServices'),
+                require('nf.ErrorHandler'),
+                require('nf.FilteredDialogCommon'),
+                require('nf.ReportingTask'),
+                require('nf.Shell'),
+                require('nf.ComponentState'),
+                require('nf.ComponentVersion'),
+                require('nf.PolicyManagement')));
+    } else {
+        nf.Settings = factory(root.$,
+            root.Slick,
+            root.d3,
+            root.nf.Client,
+            root.nf.Dialog,
+            root.nf.Storage,
+            root.nf.Common,
+            root.nf.CanvasUtils,
+            root.nf.ControllerServices,
+            root.nf.ErrorHandler,
+            root.nf.FilteredDialogCommon,
+            root.nf.ReportingTask,
+            root.nf.Shell,
+            root.nf.ComponentState,
+            root.nf.ComponentVersion,
+            root.nf.PolicyManagement);
+    }
+}(this, function ($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfControllerServices, nfErrorHandler, nfFilteredDialogCommon, nfReportingTask, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement) {
+    'use strict';
+
+
+    var config = {
+        urls: {
+            api: '../nifi-api',
+            controllerConfig: '../nifi-api/controller/config',
+            reportingTaskTypes: '../nifi-api/flow/reporting-task-types',
+            createReportingTask: '../nifi-api/controller/reporting-tasks',
+            reportingTasks: '../nifi-api/flow/reporting-tasks',
+            registries: '../nifi-api/controller/registry-clients'
+        }
+    };
+
+    var gridOptions = {
+        forceFitColumns: true,
+        enableTextSelectionOnCells: true,
+        enableCellNavigation: true,
+        enableColumnReorder: false,
+        autoEdit: false,
+        multiSelect: false,
+        rowHeight: 24
+    };
+
+
+    var dcaeDistributorApiHostname;
+
+        //get hostname
+        $.ajax({
+               type: 'GET',
+               url:   '../nifi-api/flow/config',
+               dataType: 'json',
+               contentType: 'application/json',
+               success: function(data){
+                    dcaeDistributorApiHostname= data.flowConfiguration.dcaeDistributorApiHostname;
+                    console.log(dcaeDistributorApiHostname);
+                }
+        });
+
+
+    /**
+     * Gets the controller services table.
+     *
+     * @returns {*|jQuery|HTMLElement}
+     */
+    var getDistributionEnvironmentsTable = function () {
+        return $('#distribution-environments-table');
+    };
+
+    /**
+     * Gets the controller services table.
+     *
+     * @returns {*|jQuery|HTMLElement}
+     */
+    var getControllerServicesTable = function () {
+        return $('#controller-services-table');
+    };
+
+    /**
+     * Saves the settings for the controller.
+     *
+     * @param version
+     */
+    var saveSettings = function (version) {
+        // marshal the configuration details
+        var configuration = marshalConfiguration();
+        var entity = {
+            'revision': nfClient.getRevision({
+                'revision': {
+                    'version': version
+                }
+            }),
+            'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+            'component': configuration
+        };
+
+        // save the new configuration details
+        $.ajax({
+            type: 'PUT',
+            url: config.urls.controllerConfig,
+            data: JSON.stringify(entity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (response) {
+            // close the settings dialog
+            nfDialog.showOkDialog({
+                headerText: 'Settings',
+                dialogContent: 'Settings successfully applied.'
+            });
+
+            // register the click listener for the save button
+            $('#settings-save').off('click').on('click', function () {
+                saveSettings(response.revision.version);
+            });
+        }).fail(nfErrorHandler.handleAjaxError);
+    }
+
+    /**
+     * Initializes the general tab.
+     */
+    var initGeneral = function () {
+    };
+
+    /**
+     * Marshals the details to include in the configuration request.
+     */
+    var marshalConfiguration = function () {
+        // create the configuration
+        var configuration = {};
+        configuration['maxTimerDrivenThreadCount'] = $('#maximum-timer-driven-thread-count-field').val();
+        configuration['maxEventDrivenThreadCount'] = $('#maximum-event-driven-thread-count-field').val();
+        return configuration;
+    };
+
+    /**
+     * Determines if the item matches the filter.
+     *
+     * @param {object} item     The item to filter
+     * @param {object} args     The filter criteria
+     * @returns {boolean}       Whether the item matches the filter
+     */
+    var matchesRegex = 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 matchesLabel = item['label'].search(filterExp) >= 0;
+        var matchesTags = item['tags'].search(filterExp) >= 0;
+        return matchesLabel || matchesTags;
+    };
+
+    /**
+     * Determines if the specified tags match all the tags selected by the user.
+     *
+     * @argument {string[]} tagFilters      The tag filters
+     * @argument {string} tags              The tags to test
+     */
+    var matchesSelectedTags = function (tagFilters, tags) {
+        var selectedTags = [];
+        $.each(tagFilters, function (_, filter) {
+            selectedTags.push(filter);
+        });
+
+        // normalize the tags
+        var normalizedTags = tags.toLowerCase();
+
+        var matches = true;
+        $.each(selectedTags, function (i, selectedTag) {
+            if (normalizedTags.indexOf(selectedTag) === -1) {
+                matches = false;
+                return false;
+            }
+        });
+
+        return matches;
+    };
+
+    /**
+     * Whether the specified item is selectable.
+     *
+     * @param item reporting task type
+     */
+    var isSelectable = function (item) {
+        return item.restricted === false || nfCommon.canAccessComponentRestrictions(item.explicitRestrictions);
+    };
+
+    /**
+     * Formatter for the name column.
+     *
+     * @param {type} row
+     * @param {type} cell
+     * @param {type} value
+     * @param {type} columnDef
+     * @param {type} dataContext
+     * @returns {String}
+     */
+    var nameFormatter = function (row, cell, value, columnDef, dataContext) {
+        if (!dataContext.permissions.canRead) {
+            return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
+        }
+
+        return nfCommon.escapeHtml(dataContext.component.name);
+    };
+
+    /**
+     * 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) {
+            if (a.permissions.canRead && b.permissions.canRead) {
+                if (sortDetails.columnId === 'moreDetails') {
+                    var aBulletins = 0;
+                    if (!nfCommon.isEmpty(a.bulletins)) {
+                        aBulletins = a.bulletins.length;
+                    }
+                    var bBulletins = 0;
+                    if (!nfCommon.isEmpty(b.bulletins)) {
+                        bBulletins = b.bulletins.length;
+                    }
+                    return aBulletins - bBulletins;
+                } else if (sortDetails.columnId === 'type') {
+                    var aType = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? nfCommon.substringAfterLast(a.component[sortDetails.columnId], '.') : '';
+                    var bType = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? nfCommon.substringAfterLast(b.component[sortDetails.columnId], '.') : '';
+                    return aType === bType ? 0 : aType > bType ? 1 : -1;
+                } else if (sortDetails.columnId === 'state') {
+                    var aState;
+                    if (a.component.validationStatus === 'VALIDATING') {
+                        aState = 'Validating';
+                    } else if (a.component.validationStatus === 'INVALID') {
+                        aState = 'Invalid';
+                    } else {
+                        aState = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : '';
+                    }
+                    var bState;
+                    if (b.component.validationStatus === 'VALIDATING') {
+                        bState = 'Validating';
+                    } else if (b.component.validationStatus === 'INVALID') {
+                        bState = 'Invalid';
+                    } else {
+                        bState = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : '';
+                    }
+                    return aState === bState ? 0 : aState > bState ? 1 : -1;
+                } else {
+                    var aString = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : '';
+                    var bString = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : '';
+                    return aString === bString ? 0 : aString > bString ? 1 : -1;
+                }
+            } else {
+                if (!a.permissions.canRead && !b.permissions.canRead) {
+                    return 0;
+                }
+                if (a.permissions.canRead) {
+                    return 1;
+                } else {
+                    return -1;
+                }
+            }
+        };
+
+        // perform the sort
+        data.sort(comparer, sortDetails.sortAsc);
+    };
+
+    /**
+     * Get the text out of the filter field. If the filter field doesn't
+     * have any text it will contain the text 'filter list' so this method
+     * accounts for that.
+     */
+    var getReportingTaskTypeFilterText = function () {
+        return $('#reporting-task-type-filter').val();
+    };
+
+    /**
+     * Filters the reporting task type table.
+     */
+    var applyReportingTaskTypeFilter = function () {
+        // get the dataview
+        var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+
+        // ensure the grid has been initialized
+        if (nfCommon.isDefinedAndNotNull(reportingTaskTypesGrid)) {
+            var reportingTaskTypesData = reportingTaskTypesGrid.getData();
+
+            // update the search criteria
+            reportingTaskTypesData.setFilterArgs({
+                searchString: getReportingTaskTypeFilterText()
+            });
+            reportingTaskTypesData.refresh();
+
+            // update the buttons to possibly trigger the disabled state
+            $('#new-reporting-task-dialog').modal('refreshButtons');
+
+            // update the selection if possible
+            if (reportingTaskTypesData.getLength() > 0) {
+                nfFilteredDialogCommon.choseFirstRow(reportingTaskTypesGrid);
+            }
+        }
+    };
+
+    /**
+     * Hides the selected reporting task.
+     */
+    var clearSelectedReportingTask = function () {
+        $('#reporting-task-type-description').attr('title', '').text('');
+        $('#reporting-task-type-name').attr('title', '').text('');
+        $('#reporting-task-type-bundle').attr('title', '').text('');
+        $('#selected-reporting-task-name').text('');
+        $('#selected-reporting-task-type').text('').removeData('bundle');
+        $('#reporting-task-description-container').hide();
+    };
+
+    /**
+     * Clears the selected reporting task type.
+     */
+    var clearReportingTaskSelection = function () {
+        // clear the selected row
+        clearSelectedReportingTask();
+
+        // clear the active cell the it can be reselected when its included
+        var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+        reportingTaskTypesGrid.resetActiveCell();
+    };
+
+    /**
+     * Performs the filtering.
+     *
+     * @param {object} item     The item subject to filtering
+     * @param {object} args     Filter arguments
+     * @returns {Boolean}       Whether or not to include the item
+     */
+    var filterReportingTaskTypes = function (item, args) {
+        // determine if the item matches the filter
+        var matchesFilter = matchesRegex(item, args);
+
+        // determine if the row matches the selected tags
+        var matchesTags = true;
+        if (matchesFilter) {
+            var tagFilters = $('#reporting-task-tag-cloud').tagcloud('getSelectedTags');
+            var hasSelectedTags = tagFilters.length > 0;
+            if (hasSelectedTags) {
+                matchesTags = matchesSelectedTags(tagFilters, item['tags']);
+            }
+        }
+
+        // determine if the row matches the selected source group
+        var matchesGroup = true;
+        if (matchesFilter && matchesTags) {
+            var bundleGroup = $('#reporting-task-bundle-group-combo').combo('getSelectedOption');
+            if (nfCommon.isDefinedAndNotNull(bundleGroup) && bundleGroup.value !== '') {
+                matchesGroup = (item.bundle.group === bundleGroup.value);
+            }
+        }
+
+        // determine if this row should be visible
+        var matches = matchesFilter && matchesTags && matchesGroup;
+
+        // if this row is currently selected and its being filtered
+        if (matches === false && $('#selected-reporting-task-type').text() === item['type']) {
+            clearReportingTaskSelection();
+        }
+
+        return matches;
+    };
+
+    /**
+     * Adds the currently selected reporting task.
+     */
+    var addSelectedReportingTask = function () {
+        var selectedTaskType = $('#selected-reporting-task-type').text();
+        var selectedTaskBundle = $('#selected-reporting-task-type').data('bundle');
+
+        // ensure something was selected
+        if (selectedTaskType === '') {
+            nfDialog.showOkDialog({
+                headerText: 'Settings',
+                dialogContent: 'The type of reporting task to create must be selected.'
+            });
+        } else {
+            addReportingTask(selectedTaskType, selectedTaskBundle);
+        }
+    };
+
+    /**
+     * Adds a new reporting task of the specified type.
+     *
+     * @param {string} reportingTaskType
+     * @param {object} reportingTaskBundle
+     */
+    var addReportingTask = function (reportingTaskType, reportingTaskBundle) {
+        // build the reporting task entity
+        var reportingTaskEntity = {
+            'revision': nfClient.getRevision({
+                'revision': {
+                    'version': 0
+                }
+            }),
+            'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+            'component': {
+                'type': reportingTaskType,
+                'bundle': reportingTaskBundle
+            }
+        };
+
+        // add the new reporting task
+        var addTask = $.ajax({
+            type: 'POST',
+            url: config.urls.createReportingTask,
+            data: JSON.stringify(reportingTaskEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (reportingTaskEntity) {
+            // add the item
+            var reportingTaskGrid = $('#reporting-tasks-table').data('gridInstance');
+            var reportingTaskData = reportingTaskGrid.getData();
+            reportingTaskData.addItem($.extend({
+                type: 'ReportingTask',
+                bulletins: []
+            }, reportingTaskEntity));
+
+            // resort
+            reportingTaskData.reSort();
+            reportingTaskGrid.invalidate();
+
+            // select the new reporting task
+            var row = reportingTaskData.getRowById(reportingTaskEntity.id);
+            nfFilteredDialogCommon.choseRow(reportingTaskGrid, row);
+            reportingTaskGrid.scrollRowIntoView(row);
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#new-reporting-task-dialog').modal('hide');
+
+        return addTask;
+    };
+
+    /**
+     * Adds the specified entity.
+     */
+    var addRegistry = function () {
+        var registryEntity = {
+            'revision': nfClient.getRevision({
+                'revision': {
+                    'version': 0
+                }
+            }),
+            'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+            '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.registries,
+            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;
+    };
+
+
+ /**
+     * Adds the specific environment.
+     */
+    var addDistributionEnvironment= function () {
+        var environmentEntity = {
+                'name': $('#distribution-environment-name').val(),
+                'runtimeApiUrl': $('#distribution-environment-location').val(),
+                'description': $('#distribution-environment-description').val()
+//                'nextDistributionTargetId': $('#distribution-environment-nextDistributionTargetId').val()
+      };
+
+        console.log("before POST call ");
+        console.log(environmentEntity);
+
+        // add the new distribution environment
+        var addDistributionEnvironment= $.ajax({
+            type: 'POST',
+            url: dcaeDistributorApiHostname+'/distribution-targets',
+            data: JSON.stringify(environmentEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (environmentEntity) {
+            // add the item
+            console.log("after POST call in response ");
+            console.log(environmentEntity);
+            var environmentsGrid = $('#distribution-environments-table').data('gridInstance');
+            console.log(environmentsGrid);
+            var environmentsData = environmentsGrid.getData();
+            environmentsData.addItem($.extend({
+                type: 'Environment'
+            }, environmentEntity));
+
+
+            // resort
+            environmentsData.reSort();
+            environmentsGrid.invalidate();
+
+            // select the new distribution env.
+            var row = environmentsData.getRowById(environmentEntity.id);
+            nfFilteredDialogCommon.choseRow(environmentsGrid, row);
+            registriesGrid.scrollRowIntoView(row);
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#distribution-environment-dialog').modal('hide');
+
+        return addDistributionEnvironment;
+    };
+
+
+    /**
+     * 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),
+            'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
+            '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;
+    };
+
+
+    /**
+     * Updates the distribution environment with the specified id.
+     *
+     * @param environmentId
+     */
+    var updateDistributionEnvironment   = function (environmentId) {
+        var environmentsGrid = $('#distribution-environments-table').data('gridInstance');
+        var environmentsData = environmentsGrid.getData();
+
+        var environmentEntity = environmentsData.getItemById(environmentId);
+        var requestEnvironmentEntity = {
+                'id': environmentId,
+                'name': $('#distribution-environment-name').val(),
+                'runtimeApiUrl': $('#distribution-environment-location').val(),
+                'description': $('#distribution-environment-description').val(),
+//                'nextDistributionTargetId': $('#distribution-environment-nextDistributionTargetId').val()
+        };
+
+        console.log(requestEnvironmentEntity);
+        // updating distribution environment
+        var updateDistributionEnvironment = $.ajax({
+            type: 'PUT',
+            url: dcaeDistributorApiHostname+'/distribution-targets/'+environmentEntity.id,
+            data: JSON.stringify(requestEnvironmentEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (environmentEntity) {
+            // update the item
+            console.log(environmentsGrid);
+            environmentsData.updateItem(environmentId, $.extend({
+                type: 'Environment'
+            }, environmentEntity));
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#distribution-environment-dialog').modal('hide');
+
+        return updateDistributionEnvironment;
+    };
+
+
+
+
+
+    /**
+     * Initializes the new reporting task dialog.
+     */
+    var initNewReportingTaskDialog = function () {
+        // initialize the reporting task type table
+        var reportingTaskTypesColumns = [
+            {
+                id: 'type',
+                name: 'Type',
+                field: 'label',
+                formatter: nfCommon.typeFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'version',
+                name: 'Version',
+                field: 'version',
+                formatter: nfCommon.typeVersionFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'tags',
+                name: 'Tags',
+                field: 'tags',
+                sortable: true,
+                resizable: true,
+                formatter: nfCommon.genericValueFormatter
+            }
+        ];
+
+        // initialize the dataview
+        var reportingTaskTypesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        reportingTaskTypesData.setItems([]);
+        reportingTaskTypesData.setFilterArgs({
+            searchString: getReportingTaskTypeFilterText()
+        });
+        reportingTaskTypesData.setFilter(filterReportingTaskTypes);
+
+        // initialize the sort
+        nfCommon.sortType({
+            columnId: 'type',
+            sortAsc: true
+        }, reportingTaskTypesData);
+
+        // initialize the grid
+        var reportingTaskTypesGrid = new Slick.Grid('#reporting-task-types-table', reportingTaskTypesData, reportingTaskTypesColumns, gridOptions);
+        reportingTaskTypesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        reportingTaskTypesGrid.registerPlugin(new Slick.AutoTooltips());
+        reportingTaskTypesGrid.setSortColumn('type', true);
+        reportingTaskTypesGrid.onSort.subscribe(function (e, args) {
+            nfCommon.sortType({
+                columnId: args.sortCol.field,
+                sortAsc: args.sortAsc
+            }, reportingTaskTypesData);
+        });
+        reportingTaskTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+            if ($.isArray(args.rows) && args.rows.length === 1) {
+                var reportingTaskTypeIndex = args.rows[0];
+                var reportingTaskType = reportingTaskTypesGrid.getDataItem(reportingTaskTypeIndex);
+
+                // set the reporting task type description
+                if (nfCommon.isDefinedAndNotNull(reportingTaskType)) {
+                    // show the selected reporting task
+                    $('#reporting-task-description-container').show();
+
+                    if (nfCommon.isBlank(reportingTaskType.description)) {
+                        $('#reporting-task-type-description')
+                            .attr('title', '')
+                            .html('<span class="unset">No description specified</span>');
+                    } else {
+                        $('#reporting-task-type-description')
+                            .width($('#reporting-task-description-container').innerWidth() - 1)
+                            .html(reportingTaskType.description)
+                            .ellipsis();
+                    }
+
+                    var bundle = nfCommon.formatBundle(reportingTaskType.bundle);
+                    var type = nfCommon.formatType(reportingTaskType);
+
+                    // populate the dom
+                    $('#reporting-task-type-name').text(type).attr('title', type);
+                    $('#reporting-task-type-bundle').text(bundle).attr('title', bundle);
+                    $('#selected-reporting-task-name').text(reportingTaskType.label);
+                    $('#selected-reporting-task-type').text(reportingTaskType.type).data('bundle', reportingTaskType.bundle);
+
+                    // refresh the buttons based on the current selection
+                    $('#new-reporting-task-dialog').modal('refreshButtons');
+                }
+            }
+        });
+        reportingTaskTypesGrid.onDblClick.subscribe(function (e, args) {
+            var reportingTaskType = reportingTaskTypesGrid.getDataItem(args.row);
+
+            if (isSelectable(reportingTaskType)) {
+                addReportingTask(reportingTaskType.type, reportingTaskType.bundle);
+            }
+        });
+        reportingTaskTypesGrid.onViewportChanged.subscribe(function (e, args) {
+            nfCommon.cleanUpTooltips($('#reporting-task-types-table'), 'div.view-usage-restriction');
+        });
+
+        // wire up the dataview to the grid
+        reportingTaskTypesData.onRowCountChanged.subscribe(function (e, args) {
+            reportingTaskTypesGrid.updateRowCount();
+            reportingTaskTypesGrid.render();
+
+            // update the total number of displayed processors
+            $('#displayed-reporting-task-types').text(args.current);
+        });
+        reportingTaskTypesData.onRowsChanged.subscribe(function (e, args) {
+            reportingTaskTypesGrid.invalidateRows(args.rows);
+            reportingTaskTypesGrid.render();
+        });
+        reportingTaskTypesData.syncGridSelection(reportingTaskTypesGrid, true);
+
+        // hold onto an instance of the grid
+        $('#reporting-task-types-table').data('gridInstance', reportingTaskTypesGrid).on('mouseenter', 'div.slick-cell', function (e) {
+            var usageRestriction = $(this).find('div.view-usage-restriction');
+            if (usageRestriction.length && !usageRestriction.data('qtip')) {
+                var rowId = $(this).find('span.row-id').text();
+
+                // get the status item
+                var item = reportingTaskTypesData.getItemById(rowId);
+
+                // show the tooltip
+                if (item.restricted === true) {
+                    var restrictionTip = $('<div></div>');
+
+                    if (nfCommon.isBlank(item.usageRestriction)) {
+                        restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text('Requires the following permissions:'));
+                    } else {
+                        restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text(item.usageRestriction + ' Requires the following permissions:'));
+                    }
+
+                    var restrictions = [];
+                    if (nfCommon.isDefinedAndNotNull(item.explicitRestrictions)) {
+                        $.each(item.explicitRestrictions, function (_, explicitRestriction) {
+                            var requiredPermission = explicitRestriction.requiredPermission;
+                            restrictions.push("'" + requiredPermission.label + "' - " + nfCommon.escapeHtml(explicitRestriction.explanation));
+                        });
+                    } else {
+                        restrictions.push('Access to restricted components regardless of restrictions.');
+                    }
+                    restrictionTip.append(nfCommon.formatUnorderedList(restrictions));
+
+                    usageRestriction.qtip($.extend({}, nfCommon.config.tooltipConfig, {
+                        content: restrictionTip,
+                        position: {
+                            container: $('#summary'),
+                            at: 'bottom right',
+                            my: 'top left',
+                            adjust: {
+                                x: 4,
+                                y: 4
+                            }
+                        }
+                    }));
+                }
+            }
+        });
+
+        var generalRestriction = nfCommon.getPolicyTypeListing('restricted-components');
+
+        // load the available reporting tasks
+        $.ajax({
+            type: 'GET',
+            url: config.urls.reportingTaskTypes,
+            dataType: 'json'
+        }).done(function (response) {
+            var id = 0;
+            var tags = [];
+            var groups = d3.set();
+            var restrictedUsage = d3.map();
+            var requiredPermissions = d3.map();
+
+            // begin the update
+            reportingTaskTypesData.beginUpdate();
+
+            // go through each reporting task type
+            $.each(response.reportingTaskTypes, function (i, documentedType) {
+                if (documentedType.restricted === true) {
+                    if (nfCommon.isDefinedAndNotNull(documentedType.explicitRestrictions)) {
+                        $.each(documentedType.explicitRestrictions, function (_, explicitRestriction) {
+                            var requiredPermission = explicitRestriction.requiredPermission;
+
+                            // update required permissions
+                            if (!requiredPermissions.has(requiredPermission.id)) {
+                                requiredPermissions.set(requiredPermission.id, requiredPermission.label);
+                            }
+
+                            // update component restrictions
+                            if (!restrictedUsage.has(requiredPermission.id)) {
+                                restrictedUsage.set(requiredPermission.id, []);
+                            }
+
+                            restrictedUsage.get(requiredPermission.id).push({
+                                type: nfCommon.formatType(documentedType),
+                                bundle: nfCommon.formatBundle(documentedType.bundle),
+                                explanation: nfCommon.escapeHtml(explicitRestriction.explanation)
+                            })
+                        });
+                    } else {
+                        // update required permissions
+                        if (!requiredPermissions.has(generalRestriction.value)) {
+                            requiredPermissions.set(generalRestriction.value, generalRestriction.text);
+                        }
+
+                        // update component restrictions
+                        if (!restrictedUsage.has(generalRestriction.value)) {
+                            restrictedUsage.set(generalRestriction.value, []);
+                        }
+
+                        restrictedUsage.get(generalRestriction.value).push({
+                            type: nfCommon.formatType(documentedType),
+                            bundle: nfCommon.formatBundle(documentedType.bundle),
+                            explanation: nfCommon.escapeHtml(documentedType.usageRestriction)
+                        });
+                    }
+                }
+
+                // record the group
+                groups.add(documentedType.bundle.group);
+
+                // add the documented type
+                reportingTaskTypesData.addItem({
+                    id: id++,
+                    label: nfCommon.substringAfterLast(documentedType.type, '.'),
+                    type: documentedType.type,
+                    bundle: documentedType.bundle,
+                    description: nfCommon.escapeHtml(documentedType.description),
+                    restricted:  documentedType.restricted,
+                    usageRestriction: nfCommon.escapeHtml(documentedType.usageRestriction),
+                    explicitRestrictions: documentedType.explicitRestrictions,
+                    tags: documentedType.tags.join(', ')
+                });
+
+                // count the frequency of each tag for this type
+                $.each(documentedType.tags, function (i, tag) {
+                    tags.push(tag.toLowerCase());
+                });
+            });
+
+            // end the update
+            reportingTaskTypesData.endUpdate();
+
+            // resort
+            reportingTaskTypesData.reSort();
+            reportingTaskTypesGrid.invalidate();
+
+            // set the component restrictions and the corresponding required permissions
+            nfCanvasUtils.addComponentRestrictions(restrictedUsage, requiredPermissions);
+
+            // set the total number of processors
+            $('#total-reporting-task-types, #displayed-reporting-task-types').text(response.reportingTaskTypes.length);
+
+            // create the tag cloud
+            $('#reporting-task-tag-cloud').tagcloud({
+                tags: tags,
+                select: applyReportingTaskTypeFilter,
+                remove: applyReportingTaskTypeFilter
+            });
+
+            // build the combo options
+            var options = [{
+                text: 'all groups',
+                value: ''
+            }];
+            groups.each(function (group) {
+                options.push({
+                    text: group,
+                    value: group
+                });
+            });
+
+            // initialize the bundle group combo
+            $('#reporting-task-bundle-group-combo').combo({
+                options: options,
+                select: applyReportingTaskTypeFilter
+            });
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        var navigationKeys = [$.ui.keyCode.UP, $.ui.keyCode.PAGE_UP, $.ui.keyCode.DOWN, $.ui.keyCode.PAGE_DOWN];
+
+        // define the function for filtering the list
+        $('#reporting-task-type-filter').off('keyup').on('keyup', function (e) {
+            var code = e.keyCode ? e.keyCode : e.which;
+
+            // ignore navigation keys
+            if ($.inArray(code, navigationKeys) !== -1) {
+                return;
+            }
+
+            if (code === $.ui.keyCode.ENTER) {
+                var selected = reportingTaskTypesGrid.getSelectedRows();
+
+                if (selected.length > 0) {
+                    // grid configured with multi-select = false
+                    var item = reportingTaskTypesGrid.getDataItem(selected[0]);
+                    if (isSelectable(item)) {
+                        addSelectedReportingTask();
+                    }
+                }
+            } else {
+                applyReportingTaskTypeFilter();
+            }
+        });
+
+        // setup row navigation
+        nfFilteredDialogCommon.addKeydownListener('#reporting-task-type-filter', reportingTaskTypesGrid, reportingTaskTypesGrid.getData());
+
+        // initialize the reporting task dialog
+        $('#new-reporting-task-dialog').modal({
+            scrollableContentStyle: 'scrollable',
+            headerText: 'Add Reporting Task',
+            buttons: [{
+                buttonText: 'Add',
+                color: {
+                    base: '#728E9B',
+                    hover: '#004849',
+                    text: '#ffffff'
+                },
+                disabled: function () {
+                    var selected = reportingTaskTypesGrid.getSelectedRows();
+
+                    if (selected.length > 0) {
+                        // grid configured with multi-select = false
+                        var item = reportingTaskTypesGrid.getDataItem(selected[0]);
+                        return isSelectable(item) === false;
+                    } else {
+                        return reportingTaskTypesGrid.getData().getLength() === 0;
+                    }
+                },
+                handler: {
+                    click: function () {
+                        addSelectedReportingTask();
+                    }
+                }
+            },
+                {
+                    buttonText: 'Cancel',
+                    color: {
+                        base: '#E3E8EB',
+                        hover: '#C7D2D7',
+                        text: '#004849'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+            handler: {
+                close: function () {
+                    // clear the selected row
+                    clearSelectedReportingTask();
+
+                    // clear any filter strings
+                    $('#reporting-task-type-filter').val('');
+
+                    // clear the tagcloud
+                    $('#reporting-task-tag-cloud').tagcloud('clearSelectedTags');
+
+                    // reset the group combo
+                    $('#reporting-task-bundle-group-combo').combo('setSelectedOption', {
+                        value: ''
+                    });
+
+                    // reset the filter
+                    applyReportingTaskTypeFilter();
+
+                    // unselect any current selection
+                    var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+                    reportingTaskTypesGrid.setSelectedRows([]);
+                    reportingTaskTypesGrid.resetActiveCell();
+                },
+                resize: function () {
+                    $('#reporting-task-type-description')
+                        .width($('#reporting-task-description-container').innerWidth() - 1)
+                        .text($('#reporting-task-type-description').attr('title'))
+                        .ellipsis();
+                }
+            }
+        });
+
+        // initialize the registry configuration dialog
+                   $('#registry-configuration-dialog').modal({
+                       scrollableContentStyle: 'scrollable',
+                       handler: {
+                           close: function () {
+                               $('#registry-id').text('');
+                               $('#registry-name').val('');
+                               $('#registry-location').val('');
+                               $('#registry-description').val('');
+                           }
+                       }
+                   });
+
+
+              // initialize the distribution environment  dialog
+                 $('#distribution-environment-dialog').modal({
+                     scrollableContentStyle: 'scrollable',
+                     handler: {
+                         close: function () {
+                             $('#distribution-environment-id').text('');
+                             $('#distribution-environment-name').val('');
+                             $('#distribution-environment-location').val('');
+                             $('#distribution-environment-description').val('');
+//                             $('#distribution-environment-nextDistributionTargetId').val('');
+                         }
+                     }
+                 });
+    };
+
+    /**
+     * Initializes the reporting tasks tab.
+     */
+    var initReportingTasks = function () {
+        // initialize the new reporting task dialog
+        initNewReportingTaskDialog();
+
+        var moreReportingTaskDetails = function (row, cell, value, columnDef, dataContext) {
+            if (!dataContext.permissions.canRead) {
+                return '';
+            }
+
+            var markup = '<div title="View Details" class="pointer view-reporting-task fa fa-info-circle"></div>';
+
+            // always include a button to view the usage
+            markup += '<div title="Usage" class="pointer reporting-task-usage fa fa-book"></div>';
+
+            var hasErrors = !nfCommon.isEmpty(dataContext.component.validationErrors);
+            var hasBulletins = !nfCommon.isEmpty(dataContext.bulletins);
+
+            if (hasErrors) {
+                markup += '<div class="pointer has-errors fa fa-warning" ></div>';
+            }
+
+            if (hasBulletins) {
+                markup += '<div class="has-bulletins fa fa-sticky-note-o"></div>';
+            }
+
+            if (hasErrors || hasBulletins) {
+                markup += '<span class="hidden row-id">' + nfCommon.escapeHtml(dataContext.component.id) + '</span>';
+            }
+
+            return markup;
+        };
+
+        var reportingTaskRunStatusFormatter = function (row, cell, value, columnDef, dataContext) {
+            // determine the appropriate label
+            var icon = '', label = '';
+            if (dataContext.status.validationStatus === 'VALIDATING') {
+                icon = 'validating fa fa-spin fa-circle-notch';
+                label = 'Validating';
+            } else if (dataContext.status.validationStatus === 'INVALID') {
+                icon = 'invalid fa fa-warning';
+                label = 'Invalid';
+            } else {
+                if (dataContext.status.runStatus === 'STOPPED') {
+                    label = 'Stopped';
+                    icon = 'fa fa-stop stopped';
+                } else if (dataContext.status.runStatus === 'RUNNING') {
+                    label = 'Running';
+                    icon = 'fa fa-play running';
+                } else {
+                    label = 'Disabled';
+                    icon = 'icon icon-enable-false disabled';
+                }
+            }
+
+            // include the active thread count if appropriate
+            var activeThreadCount = '';
+            if (nfCommon.isDefinedAndNotNull(dataContext.status.activeThreadCount) && dataContext.status.activeThreadCount > 0) {
+                activeThreadCount = '(' + dataContext.status.activeThreadCount + ')';
+            }
+
+            // format the markup
+            var formattedValue = '<div layout="row"><div class="' + icon + '"></div>';
+            return formattedValue + '<div class="status-text">' + nfCommon.escapeHtml(label) + '</div><div style="float: left; margin-left: 4px;">' + nfCommon.escapeHtml(activeThreadCount) + '</div></div>';
+        };
+
+        var reportingTaskActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            var canWrite = dataContext.permissions.canWrite;
+            var canRead = dataContext.permissions.canRead;
+            var canOperate = dataContext.operatePermissions.canWrite || canWrite;
+            var isStopped = dataContext.status.runStatus === 'STOPPED';
+
+            if (dataContext.status.runStatus === 'RUNNING') {
+                if (canOperate) {
+                    markup += '<div title="Stop" class="pointer stop-reporting-task fa fa-stop"></div>';
+                }
+
+            } else if (isStopped || dataContext.status.runStatus === 'DISABLED') {
+
+                if (canRead && canWrite) {
+                    markup += '<div title="Edit" class="pointer edit-reporting-task fa fa-pencil"></div>';
+                }
+
+                // support starting when stopped and no validation errors
+                if (canOperate && dataContext.status.runStatus === 'STOPPED' && dataContext.status.validationStatus === 'VALID') {
+                    markup += '<div title="Start" class="pointer start-reporting-task fa fa-play"></div>';
+                }
+
+                if (canRead && canWrite && dataContext.component.multipleVersionsAvailable === true) {
+                    markup += '<div title="Change Version" class="pointer change-version-reporting-task fa fa-exchange"></div>';
+                }
+
+                if (canRead && canWrite && nfCommon.canModifyController()) {
+                    markup += '<div title="Remove" class="pointer delete-reporting-task fa fa-trash"></div>';
+                }
+            }
+
+            if (canRead && canWrite && dataContext.component.persistsState === true) {
+                markup += '<div title="View State" class="pointer view-state-reporting-task fa fa-tasks"></div>';
+            }
+
+            // allow policy configuration conditionally
+            if (nfCanvasUtils.isManagedAuthorizer() && nfCommon.canAccessTenants()) {
+                markup += '<div title="Access Policies" class="pointer edit-access-policies fa fa-key"></div>';
+            }
+
+            return markup;
+        };
+
+        // define the column model for the reporting tasks table
+        var reportingTasksColumnModel = [
+            {
+                id: 'moreDetails',
+                name: '&nbsp;',
+                resizable: false,
+                formatter: moreReportingTaskDetails,
+                sortable: true,
+                width: 90,
+                maxWidth: 90,
+                toolTip: 'Sorts based on presence of bulletins'
+            },
+            {
+                id: 'name',
+                name: 'Name',
+                sortable: true,
+                resizable: true,
+                formatter: nameFormatter
+            },
+            {
+                id: 'type',
+                name: 'Type',
+                formatter: nfCommon.instanceTypeFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'bundle',
+                name: 'Bundle',
+                formatter: nfCommon.instanceBundleFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'state',
+                name: 'Run Status',
+                sortable: true,
+                resizeable: true,
+                formatter: reportingTaskRunStatusFormatter
+            }
+        ];
+
+        // action column should always be last
+        reportingTasksColumnModel.push({
+            id: 'actions',
+            name: '&nbsp;',
+            resizable: false,
+            formatter: reportingTaskActionFormatter,
+            sortable: false,
+            width: 90,
+            maxWidth: 90
+        });
+
+        // initialize the dataview
+        var reportingTasksData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        reportingTasksData.setItems([]);
+
+        // initialize the sort
+        sort({
+            columnId: 'name',
+            sortAsc: true
+        }, reportingTasksData);
+
+        // initialize the grid
+        var reportingTasksGrid = new Slick.Grid('#reporting-tasks-table', reportingTasksData, reportingTasksColumnModel, gridOptions);
+        reportingTasksGrid.setSelectionModel(new Slick.RowSelectionModel());
+        reportingTasksGrid.registerPlugin(new Slick.AutoTooltips());
+        reportingTasksGrid.setSortColumn('name', true);
+        reportingTasksGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.id,
+                sortAsc: args.sortAsc
+            }, reportingTasksData);
+        });
+
+        // configure a click listener
+        reportingTasksGrid.onClick.subscribe(function (e, args) {
+            var target = $(e.target);
+
+            // get the service at this row
+            var reportingTaskEntity = reportingTasksData.getItem(args.row);
+
+            // determine the desired action
+            if (reportingTasksGrid.getColumns()[args.cell].id === 'actions') {
+                if (target.hasClass('edit-reporting-task')) {
+                    nfReportingTask.showConfiguration(reportingTaskEntity);
+                } else if (target.hasClass('start-reporting-task')) {
+                    nfReportingTask.start(reportingTaskEntity);
+                } else if (target.hasClass('stop-reporting-task')) {
+                    nfReportingTask.stop(reportingTaskEntity);
+                } else if (target.hasClass('delete-reporting-task')) {
+                    nfReportingTask.promptToDeleteReportingTask(reportingTaskEntity);
+                } else if (target.hasClass('view-state-reporting-task')) {
+                    var canClear = reportingTaskEntity.status.runStatus === 'STOPPED' && reportingTaskEntity.status.activeThreadCount === 0;
+                    nfComponentState.showState(reportingTaskEntity, canClear);
+                } else if (target.hasClass('change-version-reporting-task')) {
+                    nfComponentVersion.promptForVersionChange(reportingTaskEntity);
+                } else if (target.hasClass('edit-access-policies')) {
+                    // show the policies for this service
+                    nfPolicyManagement.showReportingTaskPolicy(reportingTaskEntity);
+
+                    // close the settings dialog
+                    $('#shell-close-button').click();
+                }
+            } else if (reportingTasksGrid.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
+        reportingTasksData.onRowCountChanged.subscribe(function (e, args) {
+            reportingTasksGrid.updateRowCount();
+            reportingTasksGrid.render();
+        });
+        reportingTasksData.onRowsChanged.subscribe(function (e, args) {
+            reportingTasksGrid.invalidateRows(args.rows);
+            reportingTasksGrid.render();
+        });
+        reportingTasksData.syncGridSelection(reportingTasksGrid, true);
+
+        // hold onto an instance of the grid
+        $('#reporting-tasks-table').data('gridInstance', reportingTasksGrid).on('mouseenter', 'div.slick-cell', function (e) {
+            var errorIcon = $(this).find('div.has-errors');
+            if (errorIcon.length && !errorIcon.data('qtip')) {
+                var taskId = $(this).find('span.row-id').text();
+
+                // get the task item
+                var reportingTaskEntity = reportingTasksData.getItemById(taskId);
+
+                // format the errors
+                var tooltip = nfCommon.formatUnorderedList(reportingTaskEntity.component.validationErrors);
+
+                // show the tooltip
+                if (nfCommon.isDefinedAndNotNull(tooltip)) {
+                    errorIcon.qtip($.extend({},
+                        nfCommon.config.tooltipConfig,
+                        {
+                            content: tooltip,
+                            position: {
+                                target: 'mouse',
+                                viewport: $('#shell-container'),
+                                adjust: {
+                                    x: 8,
+                                    y: 8,
+                                    method: 'flipinvert flipinvert'
+                                }
+                            }
+                        }));
+                }
+            }
+
+            var bulletinIcon = $(this).find('div.has-bulletins');
+            if (bulletinIcon.length && !bulletinIcon.data('qtip')) {
+                var taskId = $(this).find('span.row-id').text();
+
+                // get the task item
+                var reportingTaskEntity = reportingTasksData.getItemById(taskId);
+
+                // format the tooltip
+                var bulletins = nfCommon.getFormattedBulletins(reportingTaskEntity.bulletins);
+                var tooltip = nfCommon.formatUnorderedList(bulletins);
+
+                // show the tooltip
+                if (nfCommon.isDefinedAndNotNull(tooltip)) {
+                    bulletinIcon.qtip($.extend({},
+                        nfCommon.config.tooltipConfig,
+                        {
+                            content: tooltip,
+                            position: {
+                                target: 'mouse',
+                                viewport: $('#shell-container'),
+                                adjust: {
+                                    x: 8,
+                                    y: 8,
+                                    method: 'flipinvert flipinvert'
+                                }
+                            }
+                        }));
+                }
+            }
+        });
+    };
+
+
+
+   /**
+    * Initializing Registry table
+    */
+    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"></div>';
+
+                // remove registry
+                markup += '<div title="Remove" class="pointer remove-registry fa fa-trash"></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);
+    };
+
+
+
+
+    /**
+     * Initializing Distribution Environments table
+     */
+    var initDistributionEnvironmentsTable = 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 envActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            if (nfCommon.canModifyController()) {
+                // edit environment
+                markup += '<div title="Edit" class="pointer edit-registry fa fa-pencil"></div>';
+
+                // remove environment
+                markup += '<div title="Remove" class="pointer remove-registry fa fa-trash"></div>';
+            }
+
+            return markup;
+        };
+
+          // define the column model for the Distribution environments table
+                var environmentsColumnModel = [
+                    {
+                        id: 'name',
+                        name: 'Name',
+                        field: 'name',
+//                        formatter: nameFormatter,
+                        sortable: true,
+                        resizable: true
+                    },
+                    {
+                        id: 'runtimeApiUrl',
+                        name: 'Location',
+                        field: 'runtimeApiUrl',
+//                        formatter: locationFormatter,
+                        sortable: true,
+                        resizable: true
+                    },
+                    {
+                        id: 'description',
+                        name: 'Description',
+                        field: 'description',
+//                        formatter: descriptionFormatter,
+                        sortable: true,
+                        resizable: true
+                    }
+//                    ,
+//                    {
+//                        id: 'nextDistributionTargetId',
+//                        name: 'Next Distribution TargetId',
+//                        field: 'nextDistributionTargetId',
+//                        formatter: descriptionFormatter,
+//                        sortable: true,
+//                        resizable: true
+//                    }
+                ];
+
+              // action column should always be last
+                    environmentsColumnModel.push({
+                        id: 'actions',
+                        name: '&nbsp;',
+                        resizable: false,
+                        formatter: envActionFormatter,
+                        sortable: false,
+                        width: 90,
+                        maxWidth: 90
+                    });
+
+               // initialize the dataview
+                    var environmentsData = new Slick.Data.DataView({
+                        inlineFilters: false
+                    });
+                    environmentsData.setItems([]);
+
+              // initialize the sort
+                 sort({
+                     columnId: 'name',
+                     sortAsc: true
+                 }, environmentsData);
+
+             // initialize the grid
+                 var environmentsGrid = new Slick.Grid('#distribution-environments-table', environmentsData, environmentsColumnModel, gridOptions);
+                 environmentsGrid.setSelectionModel(new Slick.RowSelectionModel());
+                 environmentsGrid.registerPlugin(new Slick.AutoTooltips());
+                 environmentsGrid.setSortColumn('name', true);
+                 environmentsGrid.onSort.subscribe(function (e, args) {
+                     sort({
+                         columnId: args.sortCol.id,
+                         sortAsc: args.sortAsc
+                     }, environmentsData);
+                 });
+
+             // configure a click listener
+                environmentsGrid.onClick.subscribe(function (e, args) {
+                    var target = $(e.target);
+
+             // get the service at this row
+               var environmentEntity = environmentsData.getItem(args.row);
+
+             // determine the desired action
+              if (environmentsGrid.getColumns()[args.cell].id === 'actions') {
+                  if (target.hasClass('edit-registry')) {                      //left it as edit-registry intentionally to inherit styles
+                      editDistributionEnvironment(environmentEntity);
+                  } else if (target.hasClass('remove-registry')) {             //left it as remove-registry intentionally to inherit styles
+                      promptToRemoveDistributionEnvironment(environmentEntity);
+                  }
+              } else if (environmentsGrid.getColumns()[args.cell].id === 'moreDetails') { }
+          });
+
+          // wire up the dataview to the grid
+            environmentsData.onRowCountChanged.subscribe(function (e, args) {
+                environmentsGrid.updateRowCount();
+                environmentsGrid.render();
+            });
+            environmentsData.onRowsChanged.subscribe(function (e, args) {
+                environmentsGrid.invalidateRows(args.rows);
+                environmentsGrid.render();
+            });
+            environmentsData.syncGridSelection(environmentsGrid, true);
+
+            // hold onto an instance of the grid
+            $('#distribution-environments-table').data('gridInstance', environmentsGrid);
+        };
+
+
+
+    /**
+     * 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('setHeaderText', 'Edit Registry Client').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');
+    };
+
+
+        /**
+         * Edits the specified distribution environment entity.
+         *@author Renu
+         * @param distributionEnvironmentEntity
+         */
+        var editDistributionEnvironment = function (distributionEnvironmentEntity) {
+            // populate the dialog
+            $('#distribution-environment-id').text(distributionEnvironmentEntity.id);
+            $('#distribution-environment-name').val(distributionEnvironmentEntity.name);
+            $('#distribution-environment-location').val(distributionEnvironmentEntity.runtimeApiUrl);
+            $('#distribution-environment-description').val(distributionEnvironmentEntity.description);
+//            $('#distribution-environment-nextDistributionTargetId ').val(distributionEnvironmentEntity.component.description);
+
+            // show the dialog
+            $('#distribution-environment-dialog').modal('setHeaderText', 'Edit Environment').modal('setButtonModel', [{
+                buttonText: 'Update',
+                color: {
+                    base: '#728E9B',
+                    hover: '#004849',
+                    text: '#ffffff'
+                },
+                handler: {
+                    click: function () {
+                        updateDistributionEnvironment(distributionEnvironmentEntity.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 environment.
+     *
+     * @param {object} environmentEntity
+     */
+    var promptToRemoveDistributionEnvironment = function (environmentEntity) {
+        // prompt for deletion
+        nfDialog.showYesNoDialog({
+            headerText: 'Delete Environment',
+            dialogContent: 'Delete Environment \'' + nfCommon.escapeHtml(environmentEntity.name) + '\'?',
+            yesHandler: function () {
+                removeDistributionEnvironment(environmentEntity);
+            }
+        });
+    };
+
+
+     /**
+      * Deletes the specified environment.
+      *
+      * @param {object} environmentEntity
+      */
+     var removeDistributionEnvironment = function (environmentEntity) {
+         console.log(environmentEntity);
+         $.ajax({
+             type: 'DELETE',
+             url: dcaeDistributorApiHostname+'/distribution-targets/'+environmentEntity.id,
+             dataType: 'json'
+         }).done(function (response) {
+          console.log(response);
+             // remove the task
+             var environmentsGrid = $('#distribution-environments-table').data('gridInstance');
+             console.log(environmentsGrid);
+             var environmentsData = environmentsGrid.getData();
+             environmentsData.deleteItem(environmentEntity.id);
+         }).fail(nfErrorHandler.handleAjaxError);
+     };
+
+
+    /**
+     * 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,
+                'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged()
+            }),
+            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.
+     */
+    var loadSettings = function () {
+        var setUnauthorizedText = function () {
+            $('#read-only-maximum-timer-driven-thread-count-field').addClass('unset').text('Unauthorized');
+            $('#read-only-maximum-event-driven-thread-count-field').addClass('unset').text('Unauthorized');
+        };
+
+        var setEditable = function (editable) {
+            if (editable) {
+                $('#general-settings div.editable').show();
+                $('#general-settings div.read-only').hide();
+                $('#settings-save').show();
+            } else {
+                $('#general-settings div.editable').hide();
+                $('#general-settings div.read-only').show();
+                $('#settings-save').hide();
+            }
+        };
+
+        var settings = $.Deferred(function (deferred) {
+            $.ajax({
+                type: 'GET',
+                url: config.urls.controllerConfig,
+                dataType: 'json'
+            }).done(function (response) {
+                if (response.permissions.canWrite) {
+                    // populate the settings
+                    $('#maximum-timer-driven-thread-count-field').removeClass('unset').val(response.component.maxTimerDrivenThreadCount);
+                    $('#maximum-event-driven-thread-count-field').removeClass('unset').val(response.component.maxEventDrivenThreadCount);
+
+                    setEditable(true);
+
+                    // register the click listener for the save button
+                    $('#settings-save').off('click').on('click', function () {
+                        saveSettings(response.revision.version);
+                    });
+                } else {
+                    if (response.permissions.canRead) {
+                        // populate the settings
+                        $('#read-only-maximum-timer-driven-thread-count-field').removeClass('unset').text(response.component.maxTimerDrivenThreadCount);
+                        $('#read-only-maximum-event-driven-thread-count-field').removeClass('unset').text(response.component.maxEventDrivenThreadCount);
+                    } else {
+                        setUnauthorizedText();
+                    }
+
+                    setEditable(false);
+                }
+                deferred.resolve();
+            }).fail(function (xhr, status, error) {
+                if (xhr.status === 403) {
+                    setUnauthorizedText();
+                    setEditable(false);
+                    deferred.resolve();
+                } else {
+                    deferred.reject(xhr, status, error);
+                }
+            });
+        }).promise();
+
+        // load the controller services
+        var controllerServicesUri = config.urls.api + '/flow/controller/controller-services';
+        var controllerServicesXhr = nfControllerServices.loadControllerServices(controllerServicesUri, getControllerServicesTable());
+
+        // load the reporting tasks
+        var reportingTasks = loadReportingTasks();
+
+        // load the registries
+        var registries = loadRegistries();
+
+        // load the distribution environments
+        var distributionEnvironments = loadDistributionEnvironments();
+
+        // return a deferred for all parts of the settings
+        return $.when(settings, controllerServicesXhr, reportingTasks).done(function (settingsResult, controllerServicesResult) {
+            var controllerServicesResponse = controllerServicesResult[0];
+
+            // update the current time
+            $('#settings-last-refreshed').text(controllerServicesResponse.currentTime);
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Loads the reporting tasks.
+     */
+    var loadReportingTasks = function () {
+        return $.ajax({
+            type: 'GET',
+            url: config.urls.reportingTasks,
+            dataType: 'json'
+        }).done(function (response) {
+            var tasks = [];
+            $.each(response.reportingTasks, function (_, task) {
+                tasks.push($.extend({
+                    type: 'ReportingTask',
+                    bulletins: []
+                }, task));
+            });
+
+            var reportingTasksElement = $('#reporting-tasks-table');
+            nfCommon.cleanUpTooltips(reportingTasksElement, 'div.has-errors');
+            nfCommon.cleanUpTooltips(reportingTasksElement, 'div.has-bulletins');
+
+            var reportingTasksGrid = reportingTasksElement.data('gridInstance');
+            var reportingTasksData = reportingTasksGrid.getData();
+
+            // update the reporting tasks
+            reportingTasksData.setItems(tasks);
+            reportingTasksData.reSort();
+            reportingTasksGrid.invalidate();
+        });
+    };
+
+    /**
+     * 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();
+        });
+    };
+
+    /**
+     * Loads the distribution environments.
+     */
+    var loadDistributionEnvironments = function(){
+        console.log("in loadDistributionEnvironments.. ");
+            return $.ajax({
+                type: 'GET',
+                url: dcaeDistributorApiHostname+'/distribution-targets',
+                dataType: 'json'
+            }).done(function (response) {
+                console.log(response);
+                var environments = [];
+                $.each(response.distributionTargets, function (_, environmentEntity) {
+                    console.log(environmentEntity);
+                    environments.push($.extend({
+                        type: 'Environment'
+                    }, environmentEntity));
+                });
+
+                console.log(environments);
+                var environmentsGrid = $('#distribution-environments-table').data('gridInstance');
+                console.log(environmentsGrid);
+                var environmentsData = environmentsGrid.getData();
+
+                // update the distribution environments
+                environmentsData.setItems(environments);
+                environmentsData.reSort();
+                environmentsGrid.invalidate();
+            });
+    };
+
+    /**
+     * Shows the process group configuration.
+     */
+    var showSettings = function () {
+        // show the settings dialog
+        nfShell.showContent('#settings').done(function () {
+            reset();
+        });
+
+        //reset content to account for possible policy changes
+        $('#settings-tabs').find('.selected-tab').click();
+
+        // adjust the table size
+        nfSettings.resetTableSize();
+    };
+
+    /**
+     * Reset state of this dialog.
+     */
+    var reset = function () {
+        // reset button state
+        $('#settings-save').mouseout();
+    };
+
+    var nfSettings = {
+        /**
+         * Initializes the settings page.
+         */
+        init: function () {
+            // initialize the settings tabs
+            $('#settings-tabs').tabbs({
+                tabStyle: 'tab',
+                selectedTabStyle: 'selected-tab',
+                scrollableTabContentStyle: 'scrollable',
+                tabs: [{
+                    name: 'General',
+                    tabContentId: 'general-settings-tab-content'
+                }, {
+                    name: 'Reporting Task Controller Services',
+                    tabContentId: 'controller-services-tab-content'
+                }, {
+                    name: 'Reporting Tasks',
+                    tabContentId: 'reporting-tasks-tab-content'
+                }, {
+                    name: 'Registry Clients',
+                    tabContentId: 'registries-tab-content'
+                },{
+                    name: 'Distribution Target Environments',
+                    tabContentId: 'distribution-environment-content'
+                  }
+                ],
+                select: function () {
+                    var tab = $(this).text();
+                    if (tab === 'General') {
+                        $('#controller-cs-availability').hide();
+                        $('#new-service-or-task').hide();
+                        $('#settings-save').show();
+                    } else {
+                        var canModifyController = false;
+                        if (nfCommon.isDefinedAndNotNull(nfCommon.currentUser)) {
+                            // only consider write permissions for creating new controller services/reporting tasks
+                            canModifyController = nfCommon.currentUser.controllerPermissions.canWrite === true;
+                        }
+
+                        if (canModifyController) {
+                            $('#new-service-or-task').show();
+                            $('div.controller-settings-table').css('top', '32px');
+
+                            // update the tooltip on the button
+                            $('#new-service-or-task').attr('title', function () {
+                                if (tab === 'Reporting Task Controller Services') {
+                                    $('#settings-save').hide();
+                                    return 'Create a new reporting task controller service';
+                                } else if (tab === 'Reporting Tasks') {
+                                    $('#settings-save').hide();
+                                    return 'Create a new reporting task';
+                                } else if (tab === 'Registry Clients') {
+                                    $('#settings-save').hide();
+                                    return 'Register a new registry client';
+                                }else if (tab === 'Distribution Target Environments') {
+                                      console.log("in env tab...");
+                                    $('#settings-save').hide();
+                                    return 'Add a new distribution environment';
+                                }
+                            });
+                        } else {
+                            $('#new-service-or-task').hide();
+                            $('div.controller-settings-table').css('top', '0');
+                        }
+
+                        if (tab === 'Reporting Task Controller Services') {
+                            $('#controller-cs-availability').show();
+                        } else if (tab === 'Reporting Tasks' || tab === 'Registry Clients'|| tab === 'Distribution Target Environments') {
+                            $('#controller-cs-availability').hide();
+                        }
+
+                        // resize the table
+                        nfSettings.resetTableSize();
+                    }
+                }
+            });
+
+            // settings refresh button
+            $('#settings-refresh-button').click(function () {
+                loadSettings();
+            });
+
+            // create a new controller service or reporting task
+            $('#new-service-or-task').on('click', function () {
+            console.log("on Add Buttton clicked");
+                var selectedTab = $('#settings-tabs li.selected-tab').text();
+                if (selectedTab === 'Reporting Task Controller Services') {
+                    var controllerServicesUri = config.urls.api + '/controller/controller-services';
+                    nfControllerServices.promptNewControllerService(controllerServicesUri, getControllerServicesTable());
+                } else if (selectedTab === 'Reporting Tasks') {
+                    $('#new-reporting-task-dialog').modal('show');
+
+                    var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+                    if (nfCommon.isDefinedAndNotNull(reportingTaskTypesGrid)) {
+                        var reportingTaskTypesData = reportingTaskTypesGrid.getData();
+
+                        // reset the canvas size after the dialog is shown
+                        reportingTaskTypesGrid.resizeCanvas();
+
+                        // select the first row if possible
+                        if (reportingTaskTypesData.getLength() > 0) {
+                            nfFilteredDialogCommon.choseFirstRow(reportingTaskTypesGrid);
+                        }
+                    }
+
+                    // set the initial focus
+                    $('#reporting-task-type-filter').focus();
+                } else if (selectedTab === 'Registry Clients') {
+                    $('#registry-configuration-dialog').modal('setHeaderText', 'Add Registry Client').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');
+
+                    // set the initial focus
+                    $('#registry-name').focus();
+                } else if (selectedTab === 'Distribution Target Environments') {
+                                    $('#distribution-environment-dialog').modal('setHeaderText', 'Add New Environment').modal('setButtonModel', [{
+                                        buttonText: 'Add',
+                                        color: {
+                                            base: '#728E9B',
+                                            hover: '#004849',
+                                            text: '#ffffff'
+                                        },
+                                        handler: {
+                                            click: function () {
+                                                addDistributionEnvironment();
+                                            }
+                                        }
+                                    }, {
+                                        buttonText: 'Cancel',
+                                        color: {
+                                            base: '#E3E8EB',
+                                            hover: '#C7D2D7',
+                                            text: '#004849'
+                                        },
+                                        handler: {
+                                            click: function () {
+                                                $(this).modal('hide');
+                                            }
+                                        }
+                                    }]).modal('show');
+
+                             $(window).resize(function() {
+                                  var x=0;
+                                  console.log(x);
+                                  $("#distribution-environment-content").text(x= x + 1);
+                              });
+
+                              $(window).resize(function() {
+                                  var x=0;
+                                  console.log(x);
+                                  $("#distribution-environments-table").text(x= x + 1);
+                                  console.log(x);
+                                  console.log("resizing...table...");
+                              });
+                                    // set the initial focus
+                                    $('#distribution-environment-name').focus();
+                                }
+            });
+
+            // initialize each tab
+            initGeneral();
+            nfControllerServices.init(getControllerServicesTable(), nfSettings.showSettings);
+            initReportingTasks();
+            initRegistriesTable();
+            initDistributionEnvironmentsTable();
+        },
+
+        /**
+         * Update the size of the grid based on its container's current size.
+         */
+        resetTableSize: function () {
+            nfControllerServices.resetTableSize(getControllerServicesTable());
+            nfControllerServices.resetTableSize(getDistributionEnvironmentsTable());
+
+            var reportingTasksGrid = $('#reporting-tasks-table').data('gridInstance');
+            if (nfCommon.isDefinedAndNotNull(reportingTasksGrid)) {
+                reportingTasksGrid.resizeCanvas();
+            }
+        },
+
+        /**
+         * Shows the settings dialog.
+         */
+        showSettings: function () {
+            return loadSettings().done(showSettings);
+        },
+
+        /**
+         * Loads the settings dialogs.
+         */
+        loadSettings: function () {
+            return loadSettings();
+        },
+
+        /**
+         * Selects the specified controller service.
+         *
+         * @param {string} controllerServiceId
+         */
+        selectControllerService: function (controllerServiceId) {
+            var controllerServiceGrid = getControllerServicesTable().data('gridInstance');
+            var controllerServiceData = controllerServiceGrid.getData();
+
+            // select the desired service
+            var row = controllerServiceData.getRowById(controllerServiceId);
+            nfFilteredDialogCommon.choseRow(controllerServiceGrid, row);
+            controllerServiceGrid.scrollRowIntoView(row);
+
+            // select the controller services tab
+            $('#settings-tabs').find('li:eq(1)').click();
+        },
+
+        /**
+         * Selects the specified reporting task.
+         *
+         * @param {string} reportingTaskId
+         */
+        selectReportingTask: function (reportingTaskId) {
+            var reportingTaskGrid = $('#reporting-tasks-table').data('gridInstance');
+            var reportingTaskData = reportingTaskGrid.getData();
+
+            // select the desired service
+            var row = reportingTaskData.getRowById(reportingTaskId);
+            nfFilteredDialogCommon.choseRow(reportingTaskGrid, row);
+            reportingTaskGrid.scrollRowIntoView(row);
+
+            // select the controller services tab
+            $('#settings-tabs').find('li:eq(2)').click();
+        },
+
+        /**
+         * Sets the controller service and reporting task bulletins in their respective tables.
+         *
+         * @param {object} controllerServiceBulletins
+         * @param {object} reportingTaskBulletins
+         */
+        setBulletins: function (controllerServiceBulletins, reportingTaskBulletins) {
+            if ($('#controller-services-table').data('gridInstance')) {
+                nfControllerServices.setBulletins(getControllerServicesTable(), controllerServiceBulletins);
+            }
+
+            // reporting tasks
+            var reportingTasksGrid = $('#reporting-tasks-table').data('gridInstance');
+            var reportingTasksData = reportingTasksGrid.getData();
+            reportingTasksData.beginUpdate();
+
+            // if there are some bulletins process them
+            if (!nfCommon.isEmpty(reportingTaskBulletins)) {
+                var reportingTaskBulletinsBySource = d3.nest()
+                    .key(function (d) {
+                        return d.sourceId;
+                    })
+                    .map(reportingTaskBulletins, d3.map);
+
+                reportingTaskBulletinsBySource.each(function (sourceBulletins, sourceId) {
+                    var reportingTask = reportingTasksData.getItemById(sourceId);
+                    if (nfCommon.isDefinedAndNotNull(reportingTask)) {
+                        reportingTasksData.updateItem(sourceId, $.extend(reportingTask, {
+                            bulletins: sourceBulletins
+                        }));
+                    }
+                });
+            } else {
+                // if there are no bulletins clear all
+                var reportingTasks = reportingTasksData.getItems();
+                $.each(reportingTasks, function (_, reportingTask) {
+                    reportingTasksData.updateItem(reportingTask.id, $.extend(reportingTask, {
+                        bulletins: []
+                    }));
+                });
+            }
+            reportingTasksData.endUpdate();
+        }
+    };
+
+    return nfSettings;
+}));
diff --git a/mod/designtool/nifi-war-to-jar/extract.sh b/mod/designtool/nifi-war-to-jar/extract.sh
new file mode 100755 (executable)
index 0000000..99e8621
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/bash
+# ==============================================================================
+# Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+# ==============================================================================
+# Licensed 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.
+# ============LICENSE_END=======================================================
+
+#
+# The nifi web api is available as a war file but not as a jar file.
+# We need to compile patches against the classes in it, so we need
+# a jar file.  This shell extracts the class files from the war and copies
+# them to target/classes so maven can package them into a jar file.
+# The jar is then used as a "provided" dependency for compiling the
+# design tool patches
+#
+
+set -euf -o pipefail
+echo Extracting classes from "$1"
+cd target
+rm -rf WEB-INF classes
+jar xf $1 WEB-INF/classes/org/apache/nifi
+mv WEB-INF/classes .
+rmdir WEB-INF
diff --git a/mod/designtool/nifi-war-to-jar/pom.xml b/mod/designtool/nifi-war-to-jar/pom.xml
new file mode 100644 (file)
index 0000000..03a5074
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<!--
+================================================================================
+Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+================================================================================
+Licensed 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.
+============LICENSE_END=========================================================
+
+-->
+<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.onap.dcaegen2.platform.mod</groupId>
+    <artifactId>designtool</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>nifi-war-to-jar</artifactId>
+  <name>dcaegen2-platform-mod-designtool-nifi-web-api-war-to-jar</name>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.2.1</version>
+        <dependencies>
+          <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-api</artifactId>
+            <version>${nifi.version}</version>
+            <type>war</type>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <includeProjectDependencies>false</includeProjectDependencies>
+          <includePluginDependencies>true</includePluginDependencies>
+          <executable>./extract.sh</executable>
+          <arguments>
+            <argument>${env.HOME}/.m2/repository/org/apache/nifi/nifi-web-api/${nifi.version}/nifi-web-api-${nifi.version}.war</argument>
+          </arguments>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>process-classes</phase>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/mod/designtool/pom.xml b/mod/designtool/pom.xml
new file mode 100644 (file)
index 0000000..330b230
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<!--
+================================================================================
+Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+================================================================================
+Licensed 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.
+============LICENSE_END=========================================================
+
+-->
+<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.onap.oparent</groupId>
+    <artifactId>oparent</artifactId>
+    <version>2.0.0</version>
+  </parent>
+  <groupId>org.onap.dcaegen2.platform.mod</groupId>
+  <artifactId>designtool</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+  <name>dcaegen2-platform-mod-designtool-parent</name>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <nifi.version>1.9.2</nifi.version>
+    <jetty.version>9.4.11.v20180605</jetty.version>
+    <org.slf4j.version>1.7.25</org.slf4j.version>
+    <maven.deploy.skip>true</maven.deploy.skip>
+    <staging.dir>${project.build.directory}/mp</staging.dir>
+    <maven.build.timestamp.format>yyyyMMdd'T'HHmmss</maven.build.timestamp.format>
+    <docker.fabric.version>0.32.0</docker.fabric.version>
+    <sonar.coverage.jacoco.xmlReportPaths>${project.reporting.outputDirectory}/jacoco-ut/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
+  </properties>
+  <modules>
+    <module>nifi-war-to-jar</module>
+    <module>designtool-web</module>
+  </modules>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.sonarsource.scanner.maven</groupId>
+        <artifactId>sonar-maven-plugin</artifactId>
+        <version>3.6.0.1398</version>
+      </plugin>
+    </plugins>
+  </build>
+</project>