From 9507f2f8d2ec616f01f5ee8825106300b95e8ddc Mon Sep 17 00:00:00 2001 From: Andrew Gauld Date: Fri, 7 Feb 2020 15:00:39 +0000 Subject: Add DCAE MOD design tool project Change-Id: I660b28ebfaa7e4b5f03a1df5fd17d126f58b7c14 Issue-ID: DCAEGEN2-1860 Signed-off-by: Andrew Gauld --- mod/designtool/README.md | 37 + mod/designtool/designtool-web/Dockerfile | 77 + mod/designtool/designtool-web/pom.xml | 255 + mod/designtool/designtool-web/sh/applypatches.sh | 107 + mod/designtool/designtool-web/sh/common.sh | 43 + mod/designtool/designtool-web/sh/start.sh | 97 + .../src/main/java/org/apache/nifi/NiFi.java | 446 ++ .../org/apache/nifi/controller/AbstractPort.java | 675 +++ .../java/org/apache/nifi/nar/DCAEAutoLoader.java | 105 + .../java/org/apache/nifi/nar/DCAEClassLoaders.java | 127 + .../java/org/apache/nifi/util/NiFiProperties.java | 1551 +++++++ .../apache/nifi/web/StandardNiFiServiceFacade.java | 4899 ++++++++++++++++++++ .../org/apache/nifi/web/api/dto/DtoFactory.java | 4354 +++++++++++++++++ .../nifi/web/api/dto/FlowConfigurationDTO.java | 182 + .../nifi/web/dao/impl/StandardConnectionDAO.java | 700 +++ .../org/apache/nifi/web/server/JettyServer.java | 1226 +++++ .../main/resources/filters/canvas-min.properties | 19 + .../src/main/webapp/WEB-INF/pages/canvas.jsp | 167 + .../WEB-INF/partials/canvas/canvas-header.jsp | 191 + .../partials/canvas/connection-configuration.jsp | 218 + .../canvas/distribution-environment-dialog.jsp | 42 + .../webapp/WEB-INF/partials/canvas/flow-status.jsp | 33 + .../webapp/WEB-INF/partials/canvas/navigation.jsp | 129 + .../WEB-INF/partials/canvas/settings-content.jsp | 86 + .../main/webapp/WEB-INF/partials/canvas/shell.jsp | 34 + .../src/main/webapp/css/navigation.css | 338 ++ .../src/main/webapp/images/dcae-logo.png | Bin 0 -> 36755 bytes .../src/main/webapp/js/jquery/dcae-mod.js | 135 + .../controllers/nf-ng-breadcrumbs-controller.js | 288 ++ .../header/components/nf-ng-processor-component.js | 1150 +++++ .../js/nf/canvas/nf-connection-configuration.js | 1587 +++++++ .../main/webapp/js/nf/canvas/nf-flow-version.js | 1990 ++++++++ .../main/webapp/js/nf/canvas/nf-process-group.js | 1744 +++++++ .../src/main/webapp/js/nf/canvas/nf-settings.js | 2373 ++++++++++ mod/designtool/nifi-war-to-jar/extract.sh | 33 + mod/designtool/nifi-war-to-jar/pom.xml | 62 + mod/designtool/pom.xml | 58 + 37 files changed, 25558 insertions(+) create mode 100644 mod/designtool/README.md create mode 100644 mod/designtool/designtool-web/Dockerfile create mode 100644 mod/designtool/designtool-web/pom.xml create mode 100755 mod/designtool/designtool-web/sh/applypatches.sh create mode 100755 mod/designtool/designtool-web/sh/common.sh create mode 100755 mod/designtool/designtool-web/sh/start.sh create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/NiFi.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java create mode 100644 mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java create mode 100644 mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp create mode 100644 mod/designtool/designtool-web/src/main/webapp/css/navigation.css create mode 100644 mod/designtool/designtool-web/src/main/webapp/images/dcae-logo.png create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/jquery/dcae-mod.js create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/header/components/nf-ng-processor-component.js create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js create mode 100644 mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js create mode 100755 mod/designtool/nifi-war-to-jar/extract.sh create mode 100644 mod/designtool/nifi-war-to-jar/pom.xml create mode 100644 mod/designtool/pom.xml diff --git a/mod/designtool/README.md b/mod/designtool/README.md new file mode 100644 index 0000000..11a9bda --- /dev/null +++ b/mod/designtool/README.md @@ -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 index 0000000..f4559ff --- /dev/null +++ b/mod/designtool/designtool-web/Dockerfile @@ -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 index 0000000..7cf0d8a --- /dev/null +++ b/mod/designtool/designtool-web/pom.xml @@ -0,0 +1,255 @@ + + + + 4.0.0 + + org.onap.dcaegen2.platform.mod + designtool + 1.0.0-SNAPSHOT + + designtool-web + war + dcaegen2-platform-mod-designtool-web + + canvas-min.properties + + + + jcenter + https://jcenter.bintray.com + + false + + + true + + + + + + org.apache.nifi + nifi-framework-cluster + ${nifi.version} + provided + + + org.apache.nifi + nifi-ui-extension + ${nifi.version} + provided + + + org.apache.nifi + nifi-jetty + ${nifi.version} + provided + + + org.apache.nifi + nifi-documentation + ${nifi.version} + provided + + + org.apache.nifi + nifi-web-content-access + ${nifi.version} + provided + + + org.slf4j + jul-to-slf4j + ${org.slf4j.version} + provided + + + org.onap.dcaegen2.platform.mod + nifi-war-to-jar + ${project.version} + provided + + + org.eclipse.jetty + jetty-server + ${jetty.version} + provided + + + org.eclipse.jetty + jetty-annotations + ${jetty.version} + provided + + + org.eclipse.jetty + jetty-deploy + ${jetty.version} + provided + + + + + src/main/resources/filters/${canvas.filter} + + + + org.eclipse.jetty + jetty-jspc-maven-plugin + ${jetty.version} + + + + jspc + + + true + true + + **/canvas.jsp + + + + + + + net.alchim31.maven + yuicompressor-maven-plugin + 1.5.1 + + + + compress + + + src/main/webapp + ${staging.dir} + false + false + true + true + + + true + ${project.build.directory}/${project.build.finalName}//nf-ng-breadcrumbs-controller-min.js + + ${staging.dir}/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js + + + + true + ${project.build.directory}/${project.build.finalName}//nf-ng-processor-component-min.js + + ${staging.dir}/js/nf/canvas/header/components/nf-ng-processor-component.js + + + + true + ${project.build.directory}/${project.build.finalName}//nf-connection-configuration-min.js + + ${staging.dir}/js/nf/canvas/nf-connection-configuration.js + + + + true + ${project.build.directory}/${project.build.finalName}//nf-flow-version-min.js + + ${staging.dir}/js/nf/canvas/nf-flow-version.js + + + + true + ${project.build.directory}/${project.build.finalName}//nf-process-group-min.js + + ${staging.dir}/js/nf/canvas/nf-process-group.js + + + + true + ${project.build.directory}/${project.build.finalName}//nf-settings-min.js + + ${staging.dir}/js/nf/canvas/nf-settings.js + + + + true + ${project.build.directory}/${project.build.finalName}/navigation-min.css + + ${staging.dir}/css/navigation.css + + + + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.2.1 + + + + src/main/webapp/WEB-INF/pages + WEB-INF/pages + + canvas.jsp + + true + + + + + + io.fabric8 + docker-maven-plugin + ${docker.fabric.version} + + true + ${docker.pull.registry} + ${docker.push.registry} + + + onap/${project.groupId}.${project.artifactId} + ${onap.nexus.dockerregistry.daily} + + ${project.basedir} + + latest + ${project.version} + ${project.version}-${maven.build.timestamp}Z + + + + + + + + + build + push + + + + + + + diff --git a/mod/designtool/designtool-web/sh/applypatches.sh b/mod/designtool/designtool-web/sh/applypatches.sh new file mode 100755 index 0000000..47de89c --- /dev/null +++ b/mod/designtool/designtool-web/sh/applypatches.sh @@ -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 index 0000000..542b777 --- /dev/null +++ b/mod/designtool/designtool-web/sh/common.sh @@ -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 index 0000000..8658983 --- /dev/null +++ b/mod/designtool/designtool-web/sh/start.sh @@ -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 index 0000000..0b033db --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/NiFi.java @@ -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 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 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 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 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 parseArgs(String[] args) { + List 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 index 0000000..6023fc2 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java @@ -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 relationships; + + private final String id; + private final ConnectableType type; + private final AtomicReference name; + private final AtomicReference position; + private final AtomicReference comments; + private final AtomicReference processGroup; + private final AtomicBoolean lossTolerant; + private final AtomicReference scheduledState; + private final AtomicInteger concurrentTaskCount; + private final AtomicReference penalizationPeriod; + private final AtomicReference yieldPeriod; + private final AtomicReference schedulingPeriod; + private final AtomicReference versionedComponentId = new AtomicReference<>(); + private final AtomicLong schedulingNanos; + private final AtomicLong yieldExpiration; + private final ProcessScheduler processScheduler; + + private final Set outgoingConnections; + private final List 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 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 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 connectionsForRelationship = source.getConnections(relationship); + if (connectionsForRelationship == null || connectionsForRelationship.isEmpty()) { + return false; + } + } + + return true; + } + + @Override + public Set getConnections() { + readLock.lock(); + try { + return Collections.unmodifiableSet(outgoingConnections); + } finally { + readLock.unlock(); + } + } + + @Override + public Set 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 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 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 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 index 0000000..ec15ba6 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java @@ -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 processed = new LinkedHashSet(); + + private LoaderTask(URI indexJsonDcaeJars, ExtensionDiscoveringManager extensionManager) { + this.indexJsonDcaeJars = indexJsonDcaeJars; + this.extensionManager = extensionManager; + } + + @Override + public void run() { + try { + List toProcess = DCAEClassLoaders.getDCAEJarsURLs(this.indexJsonDcaeJars); + toProcess.removeAll(processed); + + if (!toProcess.isEmpty()) { + Set 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 index 0000000..a4dbe77 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java @@ -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 getDCAEJarsURLs(URI indexDCAEJars) { + JsonFactory jf = new JsonFactory(); + ObjectMapper om = new ObjectMapper(); + + try { + List urls = om.readValue(jf.createParser(indexDCAEJars.toURL()), List.class); + + return urls.stream().map(u -> { + try { + Map foo = (Map) 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 createDCAEBundles(List jarURLs) { + Set 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 index 0000000..3b341ec --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java @@ -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 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 getNarLibraryDirectories() { + + List 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 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: + *

+ * - login identity provider is not populated + * - Kerberos service support is not enabled + * - openid connect is not enabled + * - knox sso is not enabled + *

+ * + * @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 getContentRepositoryPaths() { + final Map 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 getProvenanceRepositoryPaths() { + final Map 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. + *

+ * 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. + *

+ * 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 vrPropertiesPaths = new ArrayList<>(); + + final String vrPropertiesFiles = getVariableRegistryProperties(); + if (!StringUtils.isEmpty(vrPropertiesFiles)) { + + final List 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 getHttpNetworkInterfaces() { + final Map 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 getHttpsNetworkInterfaces() { + final Map 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 getProvenanceRepoEncryptionKeys() { + Map keys = new HashMap<>(); + List 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}. + *

+ * 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 getWhitelistedHostsAsList() { + String rawProperty = getProperty(WEB_PROXY_HOST, ""); + List 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}. + *

+ * 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 getWhitelistedContextPathsAsList() { + String rawProperty = getProperty(WEB_PROXY_CONTEXT_PATH, ""); + List 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 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 additionalProperties) { + final Map 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 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 index 0000000..8ad05bd --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -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 revisions, final NiFiUser user) { + for (final Revision revision : revisions) { + verifyRevision(revision, user); + } + } + + @Override + public Set getRevisionsFromGroup(final String groupId, final Function> getComponents) { + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + final Set componentIds = getComponents.apply(group); + return componentIds.stream().map(id -> revisionManager.getRevision(id)).collect(Collectors.toSet()); + } + + @Override + public Set getRevisionsFromSnippet(final String snippetId) { + final Snippet snippet = snippetDAO.getSnippet(snippetId); + final Set 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 componentIds) { + processGroupDAO.verifyScheduleComponents(groupId, state, componentIds); + } + + @Override + public void verifyEnableComponents(String processGroupId, ScheduledState state, Set componentIds) { + processGroupDAO.verifyEnableComponents(processGroupId, state, componentIds); + } + + @Override + public void verifyActivateControllerServices(final String groupId, final ControllerServiceState state, final Collection 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 snapshot = updateComponent(revision, + authorizable, + () -> accessPolicyDAO.updateAccessPolicy(accessPolicyDTO), + accessPolicy -> { + final Set users = accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set 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 groups = userGroupDAO.getUserGroupsForUser(userDTO.getId()); + final Set policies = userGroupDAO.getAccessPoliciesForUser(userDTO.getId()); + final RevisionUpdate snapshot = updateComponent(revision, + usersAuthorizable, + () -> userDAO.updateUser(userDTO), + user -> { + final Set tenantEntities = groups.stream().map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set 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 policies = userGroupDAO.getAccessPoliciesForUserGroup(userGroupDTO.getId()); + final RevisionUpdate snapshot = updateComponent(revision, + userGroupsAuthorizable, + () -> userGroupDAO.updateUserGroup(userGroupDTO), + userGroup -> { + final Set tenantEntities = userGroup.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processorNode.getIdentifier())); + final List 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 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 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 the DTO Type of the updated component + * @param the Component Type of the updated component + * @return A RevisionUpdate that represents the new configuration + */ + private RevisionUpdate updateComponent(final Revision revision, final Authorizable authorizable, final Supplier daoUpdate, final Function dtoCreation) { + try { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final RevisionUpdate updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(revision), user, new UpdateRevisionTask() { + @Override + public RevisionUpdate 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 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 revisions, final SnippetDTO snippetDto) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revisions); + + final RevisionUpdate snapshot; + try { + snapshot = revisionManager.updateRevision(revisionClaim, user, new UpdateRevisionTask() { + @Override + public RevisionUpdate 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPortNode.getIdentifier())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPortNode.getIdentifier())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroupNode.getIdentifier())); + final List 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 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 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 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 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 affectedComponentDtos = new HashSet<>(); + + final Set updatedVariableNames = getUpdatedVariables(group, variableMap); + for (final String variableName : updatedVariableNames) { + final Set 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 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 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 affectedComponentEntities = new HashSet<>(); + + final Set updatedVariableNames = getUpdatedVariables(group, variableMap); + for (final String variableName : updatedVariableNames) { + final Set affectedComponents = group.getComponentsAffectedByVariable(variableName); + affectedComponentEntities.addAll(dtoFactory.createAffectedComponentEntities(affectedComponents, revisionManager)); + } + + return affectedComponentEntities; + } + + private Set getUpdatedVariables(final ProcessGroup group, final Map newVariableValues) { + final Set updatedVariableNames = new HashSet<>(); + + final ComponentVariableRegistry registry = group.getVariableRegistry(); + for (final Map.Entry 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroupNode.getIdentifier())); + final List 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 componentRevisions) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final RevisionUpdate updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(componentRevisions.values()), user, new + UpdateRevisionTask() { + @Override + public RevisionUpdate update() { + // schedule the components + processGroupDAO.enableComponents(processGroupId, state, componentRevisions.keySet()); + + // update the revisions + final Map 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 componentRevisions) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionUpdate updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(componentRevisions.values()), user, new + UpdateRevisionTask() { + @Override + public RevisionUpdate update() { + // schedule the components + processGroupDAO.scheduleComponents(processGroupId, state, componentRevisions.keySet()); + + // update the revisions + final Map 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 serviceRevisions) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionUpdate updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(serviceRevisions.values()), user, + new UpdateRevisionTask() { + @Override + public RevisionUpdate update() { + // schedule the components + processGroupDAO.activateControllerServices(processGroupId, state, serviceRevisions.keySet()); + + // update the revisions + final Map 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 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 userGroups = user != null ? userGroupDAO.getUserGroupsForUser(userId).stream() + .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final Set 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 users = userGroup != null ? userGroup.getUsers().stream() + .map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final Set 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 userGroups = accessPolicy != null ? accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final Set 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 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() { + @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 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 affectedComponentIds) { + snippetDAO.verifyDeleteSnippetComponents(snippetId); + } + + @Override + public SnippetEntity deleteSnippet(final Set 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 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() { + @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 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processorDTO.getId())); + final List 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 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 the DTO Type + * @param the NiFi Component Type + * @return a RevisionUpdate that represents the updated configuration + */ + private RevisionUpdate createComponent(final Revision revision, final ComponentDTO componentDto, final Supplier daoCreation, final Function 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 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 tenantEntities = userGroupDAO.getUserGroupsForUser(newUser.getIdentifier()).stream() + .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set 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 tenantEntities = newUserGroup.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set 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 validationErrors = processorNode.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List 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 validationErrors = port.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List 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 validationErrors = port.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroup.getIdentifier())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroup.getIdentifier())); + final List 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 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 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 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId())); + final List 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 referenceRevisions, final String controllerServiceId, final ScheduledState scheduledState, final ControllerServiceState controllerServiceState) { + + final RevisionClaim claim = new StandardRevisionClaim(referenceRevisions.values()); + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionUpdate update = revisionManager.updateRevision(claim, user, + new UpdateRevisionTask() { + @Override + public RevisionUpdate update() { + final Set updated = controllerServiceDAO.updateControllerServiceReferencingComponents(controllerServiceId, scheduledState, controllerServiceState); + final ControllerServiceReference updatedReference = controllerServiceDAO.getControllerService(controllerServiceId).getReferences(); + + // get the revisions of the updated components + final Map 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 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 lockedIds) { + final Set visited = new HashSet<>(); + visited.add(reference.getReferencedComponent()); + findControllerServiceReferencingComponentIdentifiers(reference, visited); + + final Map 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 revisions) { + final Set 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 revisions, final Set visited) { + + final String modifier = NiFiUserUtils.getNiFiUserIdentity(); + final Set referencingComponents = reference.getReferencingComponents(); + + final Set 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 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 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 getRegistryClients() { + return registryDAO.getFlowRegistries().stream() + .map(this::createRegistryClientEntity) + .collect(Collectors.toSet()); + } + + @Override + public Set getRegistriesForUser(final NiFiUser user) { + return registryDAO.getFlowRegistriesForUser(user).stream() + .map(flowRegistry -> entityFactory.createRegistryEntity(dtoFactory.createRegistryDto(flowRegistry))) + .collect(Collectors.toSet()); + } + + @Override + public Set 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 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 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 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier())); + final List 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 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier())); + final List 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 counters = controllerFacade.getCounters(); + final Set 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 getConnections(final String groupId) { + final Set 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 bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processor.getIdentifier())); + final List 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 getProcessors(final String groupId, final boolean includeDescendants) { + final Set 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 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 getWorkQueuePrioritizerTypes() { + return controllerFacade.getFlowFileComparatorTypes(); + } + + @Override + public Set getProcessorTypes(final String bundleGroup, final String bundleArtifact, final String type) { + return controllerFacade.getFlowFileProcessorTypes(bundleGroup, bundleArtifact, type); + } + + @Override + public Set 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 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 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 bulletinEntities = new ArrayList<>(); + for (final ListIterator 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 getResources() { + final List resources = controllerFacade.getResources(); + final List 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 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 inputPortDtos = new LinkedHashSet<>(); + final Set 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 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 controllerBulletinEntities = new ArrayList<>(); + + final Authorizable controllerAuthorizable = authorizableLookup.getController(); + final boolean authorized = controllerAuthorizable.isAuthorized(authorizer, RequestAction.READ, user); + final List 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 allControllerServiceBulletins = bulletinRepository.findBulletins(controllerServiceQuery); + final List 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 allReportingTaskBulletins = bulletinRepository.findBulletins(reportingTaskQuery); + final List 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 getUsers() { + final Set 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 userGroups = userGroupDAO.getUserGroupsForUser(user.getIdentifier()).stream() + .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(enforceUserExistence)).collect(Collectors.toSet()); + final Set 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 users = userGroup.getUsers().stream().map(mapUserIdToTenantEntity(enforceGroupExistence)).collect(Collectors.toSet()); + final Set 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 getUserGroups() { + final Set 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 getLabels(final String groupId) { + final Set

+ * 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 httpsNetworkInterfaces = props.getHttpsNetworkInterfaces(); + ServerConnectorCreator 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 httpNetworkInterfaces = props.getHttpNetworkInterfaces(); + ServerConnectorCreator 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 networkInterfaces, + ServerConnectorCreator 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 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 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 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 urls = new ArrayList<>(); + + for (Connector connector : server.getConnectors()) { + if (connector instanceof ServerConnector) { + final ServerConnector serverConnector = (ServerConnector) connector; + + Set hosts = new HashSet<>(); + + // determine the hosts + if (StringUtils.isNotBlank(serverConnector.getHost())) { + hosts.add(serverConnector.getHost()); + } else { + Enumeration 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 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 webAppContexts; + private final Map mimeMappings; + private final Collection componentUiExtensionWebContexts; + private final Collection contentViewerWebContexts; + private final Map> componentUiExtensionsByType; + + public ExtensionUiInfo(final Collection webAppContexts, + final Map mimeMappings, + final Collection componentUiExtensionWebContexts, + final Collection contentViewerWebContexts, + final Map> componentUiExtensionsByType) { + this.webAppContexts = webAppContexts; + this.mimeMappings = mimeMappings; + this.componentUiExtensionWebContexts = componentUiExtensionWebContexts; + this.contentViewerWebContexts = contentViewerWebContexts; + this.componentUiExtensionsByType = componentUiExtensionsByType; + } + + public Collection getWebAppContexts() { + return webAppContexts; + } + + public Map getMimeMappings() { + return mimeMappings; + } + + public Collection getComponentUiExtensionWebContexts() { + return componentUiExtensionWebContexts; + } + + public Collection getContentViewerWebContexts() { + return contentViewerWebContexts; + } + + public Map> getComponentUiExtensionsByType() { + return componentUiExtensionsByType; + } + } +} + +@FunctionalInterface +interface ServerConnectorCreator { + 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 index 0000000..1943157 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties @@ -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= +nf.canvas.style.tags=\n\ +\n\ + \ 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 index 0000000..ccfee0c --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp @@ -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" %> + + + + NiFi + + + + ${nf.canvas.style.tags} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${nf.canvas.script.tags} + + + + + + + + + +

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + 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 index 0000000..3afbe66 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp @@ -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" %> + + +
+
+ + + + + + + + +
+ +
+
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 index 0000000..c0e368c --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp @@ -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" %> + 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 index 0000000..3d7e8d9 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp @@ -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" %> + 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 index 0000000..2efe0a1 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp @@ -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" %> +
+
+
+ +
+
+ + +
+ +
+
+
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 index 0000000..caf7278 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp @@ -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" %> + + +
+ +
+
+
+ + +
+
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 index 0000000..b540ff7 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp @@ -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" %> + 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 index 0000000..5b6c4c3 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp @@ -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" %> + 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 index 0000000..05adc45 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/css/navigation.css @@ -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 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 index 0000000..879739c --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/jquery/dcae-mod.js @@ -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 index 0000000..0005837 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js @@ -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 index 0000000..0f4b953 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/header/components/nf-ng-processor-component.js @@ -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('No description specified'); + } 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 = $('
'); + + if (nfCommon.isBlank(item.usageRestriction)) { + restrictionTip.append($('

').text('Requires the following permissions:')); + } else { + restrictionTip.append($('

').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('No description specified'); + } 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 = $('
'); + + if (nfCommon.isBlank(item.usageRestriction)) { + restrictionTip.append($('

').text('Requires the following permissions:')); + } else { + restrictionTip.append($('

').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 $('
'); + }, + + /** + * 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 index 0000000..0cdb1a1 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js @@ -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 = $('
').text(nameLabel); + var relationshipValue = $('').text(name); + return $('
' + + '
').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 = $('
  • ').append($('').text(name)).attr('id', type).addClass('ui-state-default').appendTo(prioritizerList); + + // add the description if applicable + if (nfCommon.isDefinedAndNotNull(prioritizerType.description)) { + $('
    ').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 index 0000000..3c595ca --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js @@ -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 += '
    '; + } + + 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: ' ', + 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 = $('
    ').text(percentComplete + '%'); + (nfNgBridge.injector.get('$compile')($(''))(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 index 0000000..614472c --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js @@ -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 = $('
    ').text('Tracking to "' + vci.flowName + '" version ' + vci.version + ' in "' + vci.registryName + ' - ' + vci.bucketName + '"'); + var versionControlStateTip = $('
    ').text(nfCommon.getVersionControlTooltip(vci)); + return $('
    ').append(versionControlTip).append('
    ').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 index 0000000..8c61dac --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js @@ -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 '' + nfCommon.escapeHtml(dataContext.id) + ''; + } + + 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('No description specified'); + } 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 = $('
    '); + + if (nfCommon.isBlank(item.usageRestriction)) { + restrictionTip.append($('

    ').text('Requires the following permissions:')); + } else { + restrictionTip.append($('

    ').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 = '
    '; + + // always include a button to view the usage + markup += '
    '; + + var hasErrors = !nfCommon.isEmpty(dataContext.component.validationErrors); + var hasBulletins = !nfCommon.isEmpty(dataContext.bulletins); + + if (hasErrors) { + markup += '
    '; + } + + if (hasBulletins) { + markup += '
    '; + } + + if (hasErrors || hasBulletins) { + markup += ''; + } + + 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 = '
    '; + return formattedValue + '
    ' + nfCommon.escapeHtml(label) + '
    ' + nfCommon.escapeHtml(activeThreadCount) + '
    '; + }; + + 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 += '
    '; + } + + } else if (isStopped || dataContext.status.runStatus === 'DISABLED') { + + if (canRead && canWrite) { + markup += '
    '; + } + + // support starting when stopped and no validation errors + if (canOperate && dataContext.status.runStatus === 'STOPPED' && dataContext.status.validationStatus === 'VALID') { + markup += '
    '; + } + + if (canRead && canWrite && dataContext.component.multipleVersionsAvailable === true) { + markup += '
    '; + } + + if (canRead && canWrite && nfCommon.canModifyController()) { + markup += '
    '; + } + } + + if (canRead && canWrite && dataContext.component.persistsState === true) { + markup += '
    '; + } + + // allow policy configuration conditionally + if (nfCanvasUtils.isManagedAuthorizer() && nfCommon.canAccessTenants()) { + markup += '
    '; + } + + return markup; + }; + + // define the column model for the reporting tasks table + var reportingTasksColumnModel = [ + { + id: 'moreDetails', + name: ' ', + 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: ' ', + 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 '' + nfCommon.escapeHtml(dataContext.id) + ''; + } + + return nfCommon.escapeHtml(dataContext.component.uri); + }; + + var descriptionFormatter = function (row, cell, value, columnDef, dataContext) { + if (!dataContext.permissions.canRead) { + return '' + nfCommon.escapeHtml(dataContext.id) + ''; + } + + return nfCommon.escapeHtml(dataContext.component.description); + }; + + var registriesActionFormatter = function (row, cell, value, columnDef, dataContext) { + var markup = ''; + + if (nfCommon.canModifyController()) { + // edit registry + markup += '
    '; + + // remove registry + markup += '
    '; + } + + 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: ' ', + 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 '' + nfCommon.escapeHtml(dataContext.id) + ''; +// } + + return nfCommon.escapeHtml(dataContext.component.uri); + }; + + var descriptionFormatter = function (row, cell, value, columnDef, dataContext) { +// if (!dataContext.permissions.canRead) { + return '' + nfCommon.escapeHtml(dataContext.id) + ''; +// } + + return nfCommon.escapeHtml(dataContext.component.description); + }; + + var envActionFormatter = function (row, cell, value, columnDef, dataContext) { + var markup = ''; + + if (nfCommon.canModifyController()) { + // edit environment + markup += '
    '; + + // remove environment + markup += '
    '; + } + + 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: ' ', + 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 index 0000000..99e8621 --- /dev/null +++ b/mod/designtool/nifi-war-to-jar/extract.sh @@ -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 index 0000000..03a5074 --- /dev/null +++ b/mod/designtool/nifi-war-to-jar/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + org.onap.dcaegen2.platform.mod + designtool + 1.0.0-SNAPSHOT + + nifi-war-to-jar + dcaegen2-platform-mod-designtool-nifi-web-api-war-to-jar + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + org.apache.nifi + nifi-web-api + ${nifi.version} + war + + + + false + true + ./extract.sh + + ${env.HOME}/.m2/repository/org/apache/nifi/nifi-web-api/${nifi.version}/nifi-web-api-${nifi.version}.war + + + + + process-classes + + exec + + + + + + + diff --git a/mod/designtool/pom.xml b/mod/designtool/pom.xml new file mode 100644 index 0000000..330b230 --- /dev/null +++ b/mod/designtool/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + org.onap.oparent + oparent + 2.0.0 + + org.onap.dcaegen2.platform.mod + designtool + 1.0.0-SNAPSHOT + pom + dcaegen2-platform-mod-designtool-parent + + UTF-8 + 1.9.2 + 9.4.11.v20180605 + 1.7.25 + true + ${project.build.directory}/mp + yyyyMMdd'T'HHmmss + 0.32.0 + ${project.reporting.outputDirectory}/jacoco-ut/jacoco.xml + + + nifi-war-to-jar + designtool-web + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.6.0.1398 + + + + -- cgit 1.2.3-korg